Android: Implement TLB optimization to prevent deadlocks and improve performance

This commit is contained in:
Azathothas
2025-03-16 08:21:48 +00:00
committed by github-actions[bot]
parent 452e20baff
commit 85b9b9442c
3701 changed files with 926858 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_library(input_common STATIC
drivers/camera.cpp
drivers/camera.h
drivers/keyboard.cpp
drivers/keyboard.h
drivers/mouse.cpp
drivers/mouse.h
drivers/tas_input.cpp
drivers/tas_input.h
drivers/touch_screen.cpp
drivers/touch_screen.h
drivers/udp_client.cpp
drivers/udp_client.h
drivers/virtual_amiibo.cpp
drivers/virtual_amiibo.h
drivers/virtual_gamepad.cpp
drivers/virtual_gamepad.h
helpers/stick_from_buttons.cpp
helpers/stick_from_buttons.h
helpers/touch_from_buttons.cpp
helpers/touch_from_buttons.h
helpers/udp_protocol.cpp
helpers/udp_protocol.h
input_engine.cpp
input_engine.h
input_mapping.cpp
input_mapping.h
input_poller.cpp
input_poller.h
main.cpp
main.h
precompiled_headers.h
)
if (MSVC)
target_compile_options(input_common PRIVATE
/we4242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data
/we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data
/we4800 # Implicit conversion from 'type' to bool. Possible information loss
)
else()
target_compile_options(input_common PRIVATE
-Werror=conversion
)
endif()
if (ENABLE_SDL2)
target_sources(input_common PRIVATE
drivers/joycon.cpp
drivers/joycon.h
drivers/sdl_driver.cpp
drivers/sdl_driver.h
helpers/joycon_driver.cpp
helpers/joycon_driver.h
helpers/joycon_protocol/calibration.cpp
helpers/joycon_protocol/calibration.h
helpers/joycon_protocol/common_protocol.cpp
helpers/joycon_protocol/common_protocol.h
helpers/joycon_protocol/generic_functions.cpp
helpers/joycon_protocol/generic_functions.h
helpers/joycon_protocol/joycon_types.h
helpers/joycon_protocol/irs.cpp
helpers/joycon_protocol/irs.h
helpers/joycon_protocol/nfc.cpp
helpers/joycon_protocol/nfc.h
helpers/joycon_protocol/poller.cpp
helpers/joycon_protocol/poller.h
helpers/joycon_protocol/ringcon.cpp
helpers/joycon_protocol/ringcon.h
helpers/joycon_protocol/rumble.cpp
helpers/joycon_protocol/rumble.h
)
target_link_libraries(input_common PRIVATE SDL2::SDL2)
target_compile_definitions(input_common PRIVATE HAVE_SDL2)
endif()
if (ENABLE_LIBUSB)
target_sources(input_common PRIVATE
drivers/gc_adapter.cpp
drivers/gc_adapter.h
)
target_link_libraries(input_common PRIVATE libusb::usb)
target_compile_definitions(input_common PRIVATE HAVE_LIBUSB)
endif()
create_target_directory_groups(input_common)
target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers)
if (CITRON_USE_PRECOMPILED_HEADERS)
target_precompile_headers(input_common PRIVATE precompiled_headers.h)
endif()
if (ANDROID)
target_sources(input_common PRIVATE
drivers/android.cpp
drivers/android.h
)
target_link_libraries(input_common PRIVATE android)
endif()

View File

@@ -0,0 +1,367 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <set>
#include <common/settings_input.h>
#include <common/thread.h>
#include <jni.h>
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "input_common/drivers/android.h"
namespace InputCommon {
Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
vibration_thread = std::jthread([this](std::stop_token token) {
Common::SetCurrentThreadName("Android_Vibration");
auto env = Common::Android::GetEnvForThread();
using namespace std::chrono_literals;
while (!token.stop_requested()) {
SendVibrations(env, token);
}
});
}
Android::~Android() = default;
void Android::RegisterController(jobject j_input_device) {
auto env = Common::Android::GetEnvForThread();
const std::string guid = Common::Android::GetJString(
env, static_cast<jstring>(
env->CallObjectMethod(j_input_device, Common::Android::GetCitronDeviceGetGUID())));
const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetCitronDeviceGetPort());
const auto identifier = GetIdentifier(guid, static_cast<size_t>(port));
PreSetController(identifier);
if (input_devices.find(identifier) != input_devices.end()) {
env->DeleteGlobalRef(input_devices[identifier]);
}
auto new_device = env->NewGlobalRef(j_input_device);
input_devices[identifier] = new_device;
}
void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) {
const auto identifier = GetIdentifier(guid, port);
SetButton(identifier, button_id, value);
}
void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) {
const auto identifier = GetIdentifier(guid, port);
SetAxis(identifier, axis_id, value);
}
void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
float gyro_y, float gyro_z, float accel_x, float accel_y,
float accel_z) {
const auto identifier = GetIdentifier(guid, port);
const BasicMotion motion_data{
.gyro_x = gyro_x,
.gyro_y = gyro_y,
.gyro_z = gyro_z,
.accel_x = accel_x,
.accel_y = accel_y,
.accel_z = accel_z,
.delta_timestamp = delta_timestamp,
};
SetMotion(identifier, 0, motion_data);
}
Common::Input::DriverResult Android::SetVibration(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
vibration_queue.Push(VibrationRequest{
.identifier = identifier,
.vibration = vibration,
});
return Common::Input::DriverResult::Success;
}
bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
auto device = input_devices.find(identifier);
if (device != input_devices.end()) {
return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) {
return static_cast<bool>(env->CallBooleanMethod(
device->second, Common::Android::GetCitronDeviceGetSupportsVibration()));
});
}
return false;
}
std::vector<Common::ParamPackage> Android::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
auto env = Common::Android::GetEnvForThread();
for (const auto& [key, value] : input_devices) {
auto name_object = static_cast<jstring>(
env->CallObjectMethod(value, Common::Android::GetCitronDeviceGetName()));
const std::string name =
fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port);
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"guid", key.guid.RawString()},
{"port", std::to_string(key.port)},
});
}
return devices;
}
std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const {
auto j_axes = static_cast<jobjectArray>(
env->CallObjectMethod(j_device, Common::Android::GetCitronDeviceGetAxes()));
std::set<s32> axes;
for (int i = 0; i < env->GetArrayLength(j_axes); ++i) {
jobject axis = env->GetObjectArrayElement(j_axes, i);
axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField()));
}
return axes;
}
Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y) const {
Common::ParamPackage params;
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("axis_x", axis_x);
params.Set("axis_y", axis_y);
params.Set("offset_x", 0);
params.Set("offset_y", 0);
params.Set("invert_x", "+");
// Invert Y-Axis by default
params.Set("invert_y", "-");
return params;
}
Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
bool invert) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("axis", axis);
params.Set("threshold", "0.5");
params.Set("invert", invert ? "-" : "+");
return params;
}
Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier,
s32 button) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("button", button);
return params;
}
bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const {
for (size_t i = 0; i < vids.size(); ++i) {
auto fucker = device.RawString();
if (fucker.find(vids[i]) != std::string::npos) {
return true;
}
}
return false;
}
AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
auto identifier =
GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
auto& j_device = input_devices[identifier];
if (j_device == nullptr) {
return {};
}
auto env = Common::Android::GetEnvForThread();
std::set<s32> axes = GetDeviceAxes(env, j_device);
if (axes.size() == 0) {
return {};
}
AnalogMapping mapping = {};
if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) {
mapping.insert_or_assign(Settings::NativeAnalog::LStick,
BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y));
}
if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) {
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY));
} else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) {
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ));
}
return mapping;
}
ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
auto identifier =
GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
auto& j_device = input_devices[identifier];
if (j_device == nullptr) {
return {};
}
auto env = Common::Android::GetEnvForThread();
jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size()));
env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data());
auto j_has_keys_object = static_cast<jbooleanArray>(
env->CallObjectMethod(j_device, Common::Android::GetCitronDeviceHasKeys(), j_keys));
jboolean isCopy = false;
jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy);
std::set<s32> available_keys;
for (size_t i = 0; i < keycode_ids.size(); ++i) {
if (j_has_keys[i]) {
available_keys.insert(keycode_ids[i]);
}
}
// Some devices use axes instead of buttons for certain controls so we need all the axes here
std::set<s32> axes = GetDeviceAxes(env, j_device);
ButtonMapping mapping = {};
if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) {
mapping.insert_or_assign(Settings::NativeButton::DUp,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true));
mapping.insert_or_assign(Settings::NativeButton::DDown,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false));
mapping.insert_or_assign(Settings::NativeButton::DLeft,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true));
mapping.insert_or_assign(Settings::NativeButton::DRight,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false));
} else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() &&
available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() &&
available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() &&
available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::DUp,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP));
mapping.insert_or_assign(Settings::NativeButton::DDown,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN));
mapping.insert_or_assign(Settings::NativeButton::DLeft,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT));
mapping.insert_or_assign(Settings::NativeButton::DRight,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT));
}
if (axes.find(AXIS_LTRIGGER) != axes.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton(
identifier, AXIS_LTRIGGER, false));
} else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZL,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2));
}
if (axes.find(AXIS_RTRIGGER) != axes.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton(
identifier, AXIS_RTRIGGER, false));
} else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZR,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2));
}
if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_ab_vids)) {
mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_A));
} else {
mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_A));
}
}
if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_ab_vids)) {
mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_B));
} else {
mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_B));
}
}
if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_xy_vids)) {
mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_X));
} else {
mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_X));
}
}
if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_xy_vids)) {
mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_Y));
} else {
mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_Y));
}
}
if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::L,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1));
}
if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::R,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1));
}
if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::LStick,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL));
}
if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::RStick,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR));
}
if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::Plus,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START));
}
if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::Minus,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT));
}
return mapping;
}
Common::Input::ButtonNames Android::GetUIName(
[[maybe_unused]] const Common::ParamPackage& params) const {
return Common::Input::ButtonNames::Value;
}
PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const {
return {
.guid = Common::UUID{guid},
.port = port,
.pad = 0,
};
}
void Android::SendVibrations(JNIEnv* env, std::stop_token token) {
VibrationRequest request = vibration_queue.PopWait(token);
auto device = input_devices.find(request.identifier);
if (device != input_devices.end()) {
float average_intensity = static_cast<float>(
(request.vibration.high_amplitude + request.vibration.low_amplitude) / 2.0);
env->CallVoidMethod(device->second, Common::Android::GetCitronDeviceVibrate(),
average_intensity);
}
}
} // namespace InputCommon

View File

@@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <set>
#include <common/threadsafe_queue.h>
#include <jni.h>
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A virtual controller that is always assigned to the game input
*/
class Android final : public InputEngine {
public:
explicit Android(std::string input_engine_);
~Android() override;
/**
* Registers controller number to accept new inputs.
* @param j_input_device CitronInputDevice object from the Android frontend to register.
*/
void RegisterController(jobject j_input_device);
/**
* Sets the status of a button on a specific controller.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param button_id The Android Keycode corresponding to this event.
* @param value Whether the button is pressed or not.
*/
void SetButtonState(std::string guid, size_t port, int button_id, bool value);
/**
* Sets the status of an axis on a specific controller.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param axis_id The Android axis ID corresponding to this event.
* @param value Value along the given axis.
*/
void SetAxisPosition(std::string guid, size_t port, int axis_id, float value);
/**
* Sets the status of the motion sensor on a specific controller
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param delta_timestamp Time passed since the last read.
* @param gyro_x,gyro_y,gyro_z Gyro sensor readings.
* @param accel_x,accel_y,accel_z Accelerometer sensor readings.
*/
void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z);
Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
std::vector<Common::ParamPackage> GetInputDevices() const override;
/**
* Gets the axes reported by the CitronInputDevice.
* @param env JNI environment pointer.
* @param j_device CitronInputDevice from the Android frontend.
* @return Set of the axes reported by the underlying Android InputDevice
*/
std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const;
Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y) const;
Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
bool invert) const;
Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier,
s32 button) const;
bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
private:
std::unordered_map<PadIdentifier, jobject> input_devices;
/// Returns the correct identifier corresponding to the player index
PadIdentifier GetIdentifier(const std::string& guid, size_t port) const;
/// Takes all vibrations from the queue and sends the command to the controller
void SendVibrations(JNIEnv* env, std::stop_token token);
static constexpr s32 AXIS_X = 0;
static constexpr s32 AXIS_Y = 1;
static constexpr s32 AXIS_Z = 11;
static constexpr s32 AXIS_RX = 12;
static constexpr s32 AXIS_RY = 13;
static constexpr s32 AXIS_RZ = 14;
static constexpr s32 AXIS_HAT_X = 15;
static constexpr s32 AXIS_HAT_Y = 16;
static constexpr s32 AXIS_LTRIGGER = 17;
static constexpr s32 AXIS_RTRIGGER = 18;
static constexpr s32 KEYCODE_DPAD_UP = 19;
static constexpr s32 KEYCODE_DPAD_DOWN = 20;
static constexpr s32 KEYCODE_DPAD_LEFT = 21;
static constexpr s32 KEYCODE_DPAD_RIGHT = 22;
static constexpr s32 KEYCODE_BUTTON_A = 96;
static constexpr s32 KEYCODE_BUTTON_B = 97;
static constexpr s32 KEYCODE_BUTTON_X = 99;
static constexpr s32 KEYCODE_BUTTON_Y = 100;
static constexpr s32 KEYCODE_BUTTON_L1 = 102;
static constexpr s32 KEYCODE_BUTTON_R1 = 103;
static constexpr s32 KEYCODE_BUTTON_L2 = 104;
static constexpr s32 KEYCODE_BUTTON_R2 = 105;
static constexpr s32 KEYCODE_BUTTON_THUMBL = 106;
static constexpr s32 KEYCODE_BUTTON_THUMBR = 107;
static constexpr s32 KEYCODE_BUTTON_START = 108;
static constexpr s32 KEYCODE_BUTTON_SELECT = 109;
const std::vector<s32> keycode_ids{
KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT,
KEYCODE_BUTTON_A, KEYCODE_BUTTON_B, KEYCODE_BUTTON_X, KEYCODE_BUTTON_Y,
KEYCODE_BUTTON_L1, KEYCODE_BUTTON_R1, KEYCODE_BUTTON_L2, KEYCODE_BUTTON_R2,
KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT,
};
const std::string sony_vid{"054c"};
const std::string nintendo_vid{"057e"};
const std::string razer_vid{"1532"};
const std::string redmagic_vid{"3537"};
const std::string backbone_labs_vid{"358a"};
const std::string xbox_vid{"045e"};
const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid,
redmagic_vid, backbone_labs_vid, xbox_vid};
const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid,
backbone_labs_vid, xbox_vid};
/// Queue of vibration request to controllers
Common::SPSCQueue<VibrationRequest> vibration_queue;
std::jthread vibration_thread;
};
} // namespace InputCommon

View File

@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/ranges.h>
#include "common/param_package.h"
#include "input_common/drivers/camera.h"
namespace InputCommon {
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
Camera::Camera(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(identifier);
}
void Camera::SetCameraData(std::size_t width, std::size_t height, std::span<const u32> data) {
const std::size_t desired_width = getImageWidth();
const std::size_t desired_height = getImageHeight();
status.data.resize(desired_width * desired_height);
// Resize image to desired format
for (std::size_t y = 0; y < desired_height; y++) {
for (std::size_t x = 0; x < desired_width; x++) {
const std::size_t pixel_index = y * desired_width + x;
const std::size_t old_x = width * x / desired_width;
const std::size_t old_y = height * y / desired_height;
const std::size_t data_pixel_index = old_y * width + old_x;
status.data[pixel_index] = static_cast<u8>(data[data_pixel_index] & 0xFF);
}
}
SetCamera(identifier, status);
}
std::size_t Camera::getImageWidth() const {
switch (status.format) {
case Common::Input::CameraFormat::Size320x240:
return 320;
case Common::Input::CameraFormat::Size160x120:
return 160;
case Common::Input::CameraFormat::Size80x60:
return 80;
case Common::Input::CameraFormat::Size40x30:
return 40;
case Common::Input::CameraFormat::Size20x15:
return 20;
case Common::Input::CameraFormat::None:
default:
return 0;
}
}
std::size_t Camera::getImageHeight() const {
switch (status.format) {
case Common::Input::CameraFormat::Size320x240:
return 240;
case Common::Input::CameraFormat::Size160x120:
return 120;
case Common::Input::CameraFormat::Size80x60:
return 60;
case Common::Input::CameraFormat::Size40x30:
return 30;
case Common::Input::CameraFormat::Size20x15:
return 15;
case Common::Input::CameraFormat::None:
default:
return 0;
}
}
Common::Input::DriverResult Camera::SetCameraFormat(
[[maybe_unused]] const PadIdentifier& identifier_,
const Common::Input::CameraFormat camera_format) {
status.format = camera_format;
return Common::Input::DriverResult::Success;
}
} // namespace InputCommon

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Camera final : public InputEngine {
public:
explicit Camera(std::string input_engine_);
void SetCameraData(std::size_t width, std::size_t height, std::span<const u32> data);
std::size_t getImageWidth() const;
std::size_t getImageHeight() const;
Common::Input::DriverResult SetCameraFormat(const PadIdentifier& identifier_,
Common::Input::CameraFormat camera_format) override;
private:
Common::Input::CameraStatus status{};
};
} // namespace InputCommon

View File

@@ -0,0 +1,548 @@
// SPDX-FileCopyrightText: 2014 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/ranges.h>
#include <libusb.h>
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/polyfill_thread.h"
#include "common/settings_input.h"
#include "common/thread.h"
#include "input_common/drivers/gc_adapter.h"
namespace InputCommon {
class LibUSBContext {
public:
explicit LibUSBContext() {
init_result = libusb_init(&ctx);
}
~LibUSBContext() {
libusb_exit(ctx);
}
LibUSBContext& operator=(const LibUSBContext&) = delete;
LibUSBContext(const LibUSBContext&) = delete;
LibUSBContext& operator=(LibUSBContext&&) noexcept = delete;
LibUSBContext(LibUSBContext&&) noexcept = delete;
[[nodiscard]] int InitResult() const noexcept {
return init_result;
}
[[nodiscard]] libusb_context* get() noexcept {
return ctx;
}
private:
libusb_context* ctx;
int init_result{};
};
class LibUSBDeviceHandle {
public:
explicit LibUSBDeviceHandle(libusb_context* ctx, uint16_t vid, uint16_t pid) noexcept {
handle = libusb_open_device_with_vid_pid(ctx, vid, pid);
}
~LibUSBDeviceHandle() noexcept {
if (handle) {
libusb_release_interface(handle, 1);
libusb_close(handle);
}
}
LibUSBDeviceHandle& operator=(const LibUSBDeviceHandle&) = delete;
LibUSBDeviceHandle(const LibUSBDeviceHandle&) = delete;
LibUSBDeviceHandle& operator=(LibUSBDeviceHandle&&) noexcept = delete;
LibUSBDeviceHandle(LibUSBDeviceHandle&&) noexcept = delete;
[[nodiscard]] libusb_device_handle* get() noexcept {
return handle;
}
private:
libusb_device_handle* handle{};
};
GCAdapter::GCAdapter(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
if (usb_adapter_handle) {
return;
}
LOG_DEBUG(Input, "Initialization started");
libusb_ctx = std::make_unique<LibUSBContext>();
const int init_res = libusb_ctx->InitResult();
if (init_res == LIBUSB_SUCCESS) {
adapter_scan_thread =
std::jthread([this](std::stop_token stop_token) { AdapterScanThread(stop_token); });
} else {
LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res);
}
}
GCAdapter::~GCAdapter() {
Reset();
}
void GCAdapter::AdapterInputThread(std::stop_token stop_token) {
LOG_DEBUG(Input, "Input thread started");
Common::SetCurrentThreadName("GCAdapter");
s32 payload_size{};
AdapterPayload adapter_payload{};
adapter_scan_thread = {};
while (!stop_token.stop_requested()) {
libusb_interrupt_transfer(usb_adapter_handle->get(), input_endpoint, adapter_payload.data(),
static_cast<s32>(adapter_payload.size()), &payload_size, 16);
if (IsPayloadCorrect(adapter_payload, payload_size)) {
UpdateControllers(adapter_payload);
UpdateVibrations();
}
std::this_thread::yield();
}
if (restart_scan_thread) {
adapter_scan_thread =
std::jthread([this](std::stop_token token) { AdapterScanThread(token); });
restart_scan_thread = false;
}
}
bool GCAdapter::IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size) {
if (payload_size != static_cast<s32>(adapter_payload.size()) ||
adapter_payload[0] != LIBUSB_DT_HID) {
LOG_DEBUG(Input, "Error reading payload (size: {}, type: {:02x})", payload_size,
adapter_payload[0]);
if (input_error_counter++ > 20) {
LOG_ERROR(Input, "Timeout, Is the adapter connected?");
adapter_input_thread.request_stop();
restart_scan_thread = true;
}
return false;
}
input_error_counter = 0;
return true;
}
void GCAdapter::UpdateControllers(const AdapterPayload& adapter_payload) {
for (std::size_t port = 0; port < pads.size(); ++port) {
const std::size_t offset = 1 + (9 * port);
const auto type = static_cast<ControllerTypes>(adapter_payload[offset] >> 4);
UpdatePadType(port, type);
if (DeviceConnected(port)) {
const u8 b1 = adapter_payload[offset + 1];
const u8 b2 = adapter_payload[offset + 2];
UpdateStateButtons(port, b1, b2);
UpdateStateAxes(port, adapter_payload);
}
}
}
void GCAdapter::UpdatePadType(std::size_t port, ControllerTypes pad_type) {
if (pads[port].type == pad_type) {
return;
}
// Device changed reset device and set new type
pads[port].axis_origin = {};
pads[port].reset_origin_counter = {};
pads[port].enable_vibration = {};
pads[port].rumble_amplitude = {};
pads[port].type = pad_type;
}
void GCAdapter::UpdateStateButtons(std::size_t port, [[maybe_unused]] u8 b1,
[[maybe_unused]] u8 b2) {
if (port >= pads.size()) {
return;
}
static constexpr std::array<PadButton, 8> b1_buttons{
PadButton::ButtonA, PadButton::ButtonB, PadButton::ButtonX, PadButton::ButtonY,
PadButton::ButtonLeft, PadButton::ButtonRight, PadButton::ButtonDown, PadButton::ButtonUp,
};
static constexpr std::array<PadButton, 4> b2_buttons{
PadButton::ButtonStart,
PadButton::TriggerZ,
PadButton::TriggerR,
PadButton::TriggerL,
};
for (std::size_t i = 0; i < b1_buttons.size(); ++i) {
const bool button_status = (b1 & (1U << i)) != 0;
const int button = static_cast<int>(b1_buttons[i]);
SetButton(pads[port].identifier, button, button_status);
}
for (std::size_t j = 0; j < b2_buttons.size(); ++j) {
const bool button_status = (b2 & (1U << j)) != 0;
const int button = static_cast<int>(b2_buttons[j]);
SetButton(pads[port].identifier, button, button_status);
}
}
void GCAdapter::UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload) {
if (port >= pads.size()) {
return;
}
const std::size_t offset = 1 + (9 * port);
static constexpr std::array<PadAxes, 6> axes{
PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX,
PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight,
};
for (const PadAxes axis : axes) {
const auto index = static_cast<std::size_t>(axis);
const u8 axis_value = adapter_payload[offset + 3 + index];
if (pads[port].reset_origin_counter <= 18) {
if (pads[port].axis_origin[index] != axis_value) {
pads[port].reset_origin_counter = 0;
}
pads[port].axis_origin[index] = axis_value;
pads[port].reset_origin_counter++;
}
const f32 axis_status = (axis_value - pads[port].axis_origin[index]) / 100.0f;
SetAxis(pads[port].identifier, static_cast<int>(index), axis_status);
}
}
void GCAdapter::AdapterScanThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("ScanGCAdapter");
usb_adapter_handle = nullptr;
pads = {};
while (!Setup() && Common::StoppableTimedWait(stop_token, std::chrono::seconds{2})) {
}
}
bool GCAdapter::Setup() {
constexpr u16 nintendo_vid = 0x057e;
constexpr u16 gc_adapter_pid = 0x0337;
usb_adapter_handle =
std::make_unique<LibUSBDeviceHandle>(libusb_ctx->get(), nintendo_vid, gc_adapter_pid);
if (!usb_adapter_handle->get()) {
return false;
}
if (!CheckDeviceAccess()) {
usb_adapter_handle = nullptr;
return false;
}
libusb_device* const device = libusb_get_device(usb_adapter_handle->get());
LOG_INFO(Input, "GC adapter is now connected");
// GC Adapter found and accessible, registering it
if (GetGCEndpoint(device)) {
rumble_enabled = true;
input_error_counter = 0;
output_error_counter = 0;
std::size_t port = 0;
for (GCController& pad : pads) {
pad.identifier = {
.guid = Common::UUID{},
.port = port++,
.pad = 0,
};
PreSetController(pad.identifier);
}
adapter_input_thread =
std::jthread([this](std::stop_token stop_token) { AdapterInputThread(stop_token); });
return true;
}
return false;
}
bool GCAdapter::CheckDeviceAccess() {
s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle->get(), 0);
if (kernel_driver_error == 1) {
kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle->get(), 0);
if (kernel_driver_error != 0 && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
LOG_ERROR(Input, "libusb_detach_kernel_driver failed with error = {}",
kernel_driver_error);
}
}
if (kernel_driver_error && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
usb_adapter_handle = nullptr;
return false;
}
const int interface_claim_error = libusb_claim_interface(usb_adapter_handle->get(), 0);
if (interface_claim_error) {
LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error);
usb_adapter_handle = nullptr;
return false;
}
// This fixes payload problems from offbrand GCAdapters
const s32 control_transfer_error =
libusb_control_transfer(usb_adapter_handle->get(), 0x21, 11, 0x0001, 0, nullptr, 0, 1000);
if (control_transfer_error < 0) {
LOG_ERROR(Input, "libusb_control_transfer failed with error= {}", control_transfer_error);
}
return true;
}
bool GCAdapter::GetGCEndpoint(libusb_device* device) {
libusb_config_descriptor* config = nullptr;
const int config_descriptor_return = libusb_get_config_descriptor(device, 0, &config);
if (config_descriptor_return != LIBUSB_SUCCESS) {
LOG_ERROR(Input, "libusb_get_config_descriptor failed with error = {}",
config_descriptor_return);
return false;
}
for (u8 ic = 0; ic < config->bNumInterfaces; ic++) {
const libusb_interface* interfaceContainer = &config->interface[ic];
for (int i = 0; i < interfaceContainer->num_altsetting; i++) {
const libusb_interface_descriptor* interface = &interfaceContainer->altsetting[i];
for (u8 e = 0; e < interface->bNumEndpoints; e++) {
const libusb_endpoint_descriptor* endpoint = &interface->endpoint[e];
if ((endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) != 0) {
input_endpoint = endpoint->bEndpointAddress;
} else {
output_endpoint = endpoint->bEndpointAddress;
}
}
}
}
// This transfer seems to be responsible for clearing the state of the adapter
// Used to clear the "busy" state of when the device is unexpectedly unplugged
unsigned char clear_payload = 0x13;
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, &clear_payload,
sizeof(clear_payload), nullptr, 16);
return true;
}
Common::Input::DriverResult GCAdapter::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f;
const auto processed_amplitude =
static_cast<u8>((mean_amplitude + std::pow(mean_amplitude, 0.3f)) * 0.5f * 0x8);
pads[identifier.port].rumble_amplitude = processed_amplitude;
if (!rumble_enabled) {
return Common::Input::DriverResult::Disabled;
}
return Common::Input::DriverResult::Success;
}
bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
return rumble_enabled;
}
void GCAdapter::UpdateVibrations() {
// Use 8 states to keep the switching between on/off fast enough for
// a human to feel different vibration strength
// More states == more rumble strengths == slower update time
constexpr u8 vibration_states = 8;
vibration_counter = (vibration_counter + 1) % vibration_states;
for (GCController& pad : pads) {
const bool vibrate = pad.rumble_amplitude > vibration_counter;
vibration_changed |= vibrate != pad.enable_vibration;
pad.enable_vibration = vibrate;
}
SendVibrations();
}
void GCAdapter::SendVibrations() {
if (!rumble_enabled || !vibration_changed) {
return;
}
s32 size{};
constexpr u8 rumble_command = 0x11;
const u8 p1 = pads[0].enable_vibration;
const u8 p2 = pads[1].enable_vibration;
const u8 p3 = pads[2].enable_vibration;
const u8 p4 = pads[3].enable_vibration;
std::array<u8, 5> payload = {rumble_command, p1, p2, p3, p4};
const int err =
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, payload.data(),
static_cast<s32>(payload.size()), &size, 16);
if (err) {
LOG_DEBUG(Input, "Libusb write failed: {}", libusb_error_name(err));
if (output_error_counter++ > 5) {
LOG_ERROR(Input, "Output timeout, Rumble disabled");
rumble_enabled = false;
}
return;
}
output_error_counter = 0;
vibration_changed = false;
}
bool GCAdapter::DeviceConnected(std::size_t port) const {
return pads[port].type != ControllerTypes::None;
}
void GCAdapter::Reset() {
adapter_scan_thread = {};
adapter_input_thread = {};
usb_adapter_handle = nullptr;
pads = {};
libusb_ctx = nullptr;
}
std::vector<Common::ParamPackage> GCAdapter::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
for (std::size_t port = 0; port < pads.size(); ++port) {
if (!DeviceConnected(port)) {
continue;
}
Common::ParamPackage identifier{};
identifier.Set("engine", GetEngineName());
identifier.Set("display", fmt::format("Gamecube Controller {}", port + 1));
identifier.Set("port", static_cast<int>(port));
devices.emplace_back(identifier);
}
return devices;
}
ButtonMapping GCAdapter::GetButtonMappingForDevice(const Common::ParamPackage& params) {
// This list is missing ZL/ZR since those are not considered buttons.
// We will add those afterwards
// This list also excludes any button that can't be really mapped
static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 14>
switch_to_gcadapter_button = {
std::pair{Settings::NativeButton::A, PadButton::ButtonA},
{Settings::NativeButton::B, PadButton::ButtonB},
{Settings::NativeButton::X, PadButton::ButtonX},
{Settings::NativeButton::Y, PadButton::ButtonY},
{Settings::NativeButton::Plus, PadButton::ButtonStart},
{Settings::NativeButton::DLeft, PadButton::ButtonLeft},
{Settings::NativeButton::DUp, PadButton::ButtonUp},
{Settings::NativeButton::DRight, PadButton::ButtonRight},
{Settings::NativeButton::DDown, PadButton::ButtonDown},
{Settings::NativeButton::SLLeft, PadButton::TriggerL},
{Settings::NativeButton::SRLeft, PadButton::TriggerR},
{Settings::NativeButton::SLRight, PadButton::TriggerL},
{Settings::NativeButton::SRRight, PadButton::TriggerR},
{Settings::NativeButton::R, PadButton::TriggerZ},
};
if (!params.Has("port")) {
return {};
}
ButtonMapping mapping{};
for (const auto& [switch_button, gcadapter_button] : switch_to_gcadapter_button) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("button", static_cast<int>(gcadapter_button));
mapping.insert_or_assign(switch_button, std::move(button_params));
}
// Add the missing bindings for ZL/ZR
static constexpr std::array<std::tuple<Settings::NativeButton::Values, PadButton, PadAxes>, 2>
switch_to_gcadapter_axis = {
std::tuple{Settings::NativeButton::ZL, PadButton::TriggerL, PadAxes::TriggerLeft},
{Settings::NativeButton::ZR, PadButton::TriggerR, PadAxes::TriggerRight},
};
for (const auto& [switch_button, gcadapter_button, gcadapter_axis] : switch_to_gcadapter_axis) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("button", static_cast<s32>(gcadapter_button));
button_params.Set("axis", static_cast<s32>(gcadapter_axis));
button_params.Set("threshold", 0.5f);
button_params.Set("range", 1.9f);
button_params.Set("direction", "+");
mapping.insert_or_assign(switch_button, std::move(button_params));
}
return mapping;
}
AnalogMapping GCAdapter::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return {};
}
AnalogMapping mapping = {};
Common::ParamPackage left_analog_params;
left_analog_params.Set("engine", GetEngineName());
left_analog_params.Set("port", params.Get("port", 0));
left_analog_params.Set("axis_x", static_cast<int>(PadAxes::StickX));
left_analog_params.Set("axis_y", static_cast<int>(PadAxes::StickY));
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("port", params.Get("port", 0));
right_analog_params.Set("axis_x", static_cast<int>(PadAxes::SubstickX));
right_analog_params.Set("axis_y", static_cast<int>(PadAxes::SubstickY));
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
return mapping;
}
Common::Input::ButtonNames GCAdapter::GetUIButtonName(const Common::ParamPackage& params) const {
PadButton button = static_cast<PadButton>(params.Get("button", 0));
switch (button) {
case PadButton::ButtonLeft:
return Common::Input::ButtonNames::ButtonLeft;
case PadButton::ButtonRight:
return Common::Input::ButtonNames::ButtonRight;
case PadButton::ButtonDown:
return Common::Input::ButtonNames::ButtonDown;
case PadButton::ButtonUp:
return Common::Input::ButtonNames::ButtonUp;
case PadButton::TriggerZ:
return Common::Input::ButtonNames::TriggerZ;
case PadButton::TriggerR:
return Common::Input::ButtonNames::TriggerR;
case PadButton::TriggerL:
return Common::Input::ButtonNames::TriggerL;
case PadButton::ButtonA:
return Common::Input::ButtonNames::ButtonA;
case PadButton::ButtonB:
return Common::Input::ButtonNames::ButtonB;
case PadButton::ButtonX:
return Common::Input::ButtonNames::ButtonX;
case PadButton::ButtonY:
return Common::Input::ButtonNames::ButtonY;
case PadButton::ButtonStart:
return Common::Input::ButtonNames::ButtonStart;
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames GCAdapter::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
return Common::Input::ButtonNames::Invalid;
}
bool GCAdapter::IsStickInverted(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return false;
}
const auto x_axis = static_cast<PadAxes>(params.Get("axis_x", 0));
const auto y_axis = static_cast<PadAxes>(params.Get("axis_y", 0));
if (x_axis != PadAxes::StickY && x_axis != PadAxes::SubstickY) {
return false;
}
if (y_axis != PadAxes::StickX && y_axis != PadAxes::SubstickX) {
return false;
}
return true;
}
} // namespace InputCommon

View File

@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2014 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <string>
#include <thread>
#include "common/polyfill_thread.h"
#include "input_common/input_engine.h"
struct libusb_context;
struct libusb_device;
struct libusb_device_handle;
namespace InputCommon {
class LibUSBContext;
class LibUSBDeviceHandle;
class GCAdapter : public InputEngine {
public:
explicit GCAdapter(std::string input_engine_);
~GCAdapter() override;
Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
bool IsStickInverted(const Common::ParamPackage& params) override;
private:
enum class PadButton {
Undefined = 0x0000,
ButtonLeft = 0x0001,
ButtonRight = 0x0002,
ButtonDown = 0x0004,
ButtonUp = 0x0008,
TriggerZ = 0x0010,
TriggerR = 0x0020,
TriggerL = 0x0040,
ButtonA = 0x0100,
ButtonB = 0x0200,
ButtonX = 0x0400,
ButtonY = 0x0800,
ButtonStart = 0x1000,
};
enum class PadAxes : u8 {
StickX,
StickY,
SubstickX,
SubstickY,
TriggerLeft,
TriggerRight,
Undefined,
};
enum class ControllerTypes {
None,
Wired,
Wireless,
};
struct GCController {
ControllerTypes type = ControllerTypes::None;
PadIdentifier identifier{};
bool enable_vibration = false;
u8 rumble_amplitude{};
std::array<u8, 6> axis_origin{};
u8 reset_origin_counter{};
};
using AdapterPayload = std::array<u8, 37>;
void UpdatePadType(std::size_t port, ControllerTypes pad_type);
void UpdateControllers(const AdapterPayload& adapter_payload);
void UpdateStateButtons(std::size_t port, u8 b1, u8 b2);
void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload);
void AdapterInputThread(std::stop_token stop_token);
void AdapterScanThread(std::stop_token stop_token);
bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size);
/// For use in initialization, querying devices to find the adapter
bool Setup();
/// Returns true if we successfully gain access to GC Adapter
bool CheckDeviceAccess();
/// Captures GC Adapter endpoint address
/// Returns true if the endpoint was set correctly
bool GetGCEndpoint(libusb_device* device);
/// Returns true if there is a device connected to port
bool DeviceConnected(std::size_t port) const;
/// For shutting down, clear all data, join all threads, release usb
void Reset();
void UpdateVibrations();
/// Updates vibration state of all controllers
void SendVibrations();
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
std::unique_ptr<LibUSBDeviceHandle> usb_adapter_handle;
std::array<GCController, 4> pads;
std::jthread adapter_input_thread;
std::jthread adapter_scan_thread;
bool restart_scan_thread{};
std::unique_ptr<LibUSBContext> libusb_ctx;
u8 input_endpoint{0};
u8 output_endpoint{0};
u8 input_error_counter{0};
u8 output_error_counter{0};
int vibration_counter{0};
bool rumble_enabled{true};
bool vibration_changed{true};
};
} // namespace InputCommon

View File

@@ -0,0 +1,843 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/ranges.h>
#include "common/param_package.h"
#include "common/polyfill_ranges.h"
#include "common/polyfill_thread.h"
#include "common/settings.h"
#include "common/thread.h"
#include "input_common/drivers/joycon.h"
#include "input_common/helpers/joycon_driver.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon {
Joycons::Joycons(const std::string& input_engine_) : InputEngine(input_engine_) {
// Avoid conflicting with SDL driver
if (!Settings::values.enable_joycon_driver && !Settings::values.enable_procon_driver) {
return;
}
LOG_INFO(Input, "Joycon driver Initialization started");
const int init_res = SDL_hid_init();
if (init_res == 0) {
Setup();
} else {
LOG_ERROR(Input, "Hidapi could not be initialized. failed with error = {}", init_res);
}
}
Joycons::~Joycons() {
Reset();
}
void Joycons::Reset() {
scan_thread = {};
for (const auto& device : left_joycons) {
if (!device) {
continue;
}
device->Stop();
}
for (const auto& device : right_joycons) {
if (!device) {
continue;
}
device->Stop();
}
for (const auto& device : pro_controller) {
if (!device) {
continue;
}
device->Stop();
}
SDL_hid_exit();
}
void Joycons::Setup() {
u32 port = 0;
PreSetController(GetIdentifier(0, Joycon::ControllerType::None));
for (auto& device : left_joycons) {
PreSetController(GetIdentifier(port, Joycon::ControllerType::Left));
device = std::make_shared<Joycon::JoyconDriver>(port++);
}
port = 0;
for (auto& device : right_joycons) {
PreSetController(GetIdentifier(port, Joycon::ControllerType::Right));
device = std::make_shared<Joycon::JoyconDriver>(port++);
}
port = 0;
for (auto& device : pro_controller) {
PreSetController(GetIdentifier(port, Joycon::ControllerType::Pro));
device = std::make_shared<Joycon::JoyconDriver>(port++);
}
scan_thread = std::jthread([this](std::stop_token stop_token) { ScanThread(stop_token); });
}
void Joycons::ScanThread(std::stop_token stop_token) {
constexpr u16 nintendo_vendor_id = 0x057e;
Common::SetCurrentThreadName("JoyconScanThread");
do {
SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0);
SDL_hid_device_info* cur_dev = devs;
while (cur_dev) {
if (IsDeviceNew(cur_dev)) {
LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id,
cur_dev->product_id);
RegisterNewDevice(cur_dev);
}
cur_dev = cur_dev->next;
}
SDL_hid_free_enumeration(devs);
} while (Common::StoppableTimedWait(stop_token, std::chrono::seconds{5}));
}
bool Joycons::IsDeviceNew(SDL_hid_device_info* device_info) const {
Joycon::ControllerType type{};
Joycon::SerialNumber serial_number{};
const auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
if (result != Common::Input::DriverResult::Success) {
return false;
}
const auto result2 = Joycon::JoyconDriver::GetSerialNumber(device_info, serial_number);
if (result2 != Common::Input::DriverResult::Success) {
return false;
}
auto is_handle_identical = [serial_number](std::shared_ptr<Joycon::JoyconDriver> device) {
if (!device) {
return false;
}
if (!device->IsConnected()) {
return false;
}
if (device->GetHandleSerialNumber() != serial_number) {
return false;
}
return true;
};
// Check if device already exist
switch (type) {
case Joycon::ControllerType::Left:
if (!Settings::values.enable_joycon_driver) {
return false;
}
for (const auto& device : left_joycons) {
if (is_handle_identical(device)) {
return false;
}
}
break;
case Joycon::ControllerType::Right:
if (!Settings::values.enable_joycon_driver) {
return false;
}
for (const auto& device : right_joycons) {
if (is_handle_identical(device)) {
return false;
}
}
break;
case Joycon::ControllerType::Pro:
if (!Settings::values.enable_procon_driver) {
return false;
}
for (const auto& device : pro_controller) {
if (is_handle_identical(device)) {
return false;
}
}
break;
default:
return false;
}
return true;
}
void Joycons::RegisterNewDevice(SDL_hid_device_info* device_info) {
Joycon::ControllerType type{};
auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
auto handle = GetNextFreeHandle(type);
if (handle == nullptr) {
LOG_WARNING(Input, "No free handles available");
return;
}
if (result == Common::Input::DriverResult::Success) {
result = handle->RequestDeviceAccess(device_info);
}
if (result == Common::Input::DriverResult::Success) {
LOG_WARNING(Input, "Initialize device");
const std::size_t port = handle->GetDevicePort();
const Joycon::JoyconCallbacks callbacks{
.on_battery_data = {[this, port, type](Joycon::Battery value) {
OnBatteryUpdate(port, type, value);
}},
.on_color_data = {[this, port, type](Joycon::Color value) {
OnColorUpdate(port, type, value);
}},
.on_button_data = {[this, port, type](int id, bool value) {
OnButtonUpdate(port, type, id, value);
}},
.on_stick_data = {[this, port, type](int id, f32 value) {
OnStickUpdate(port, type, id, value);
}},
.on_motion_data = {[this, port, type](int id, const Joycon::MotionData& value) {
OnMotionUpdate(port, type, id, value);
}},
.on_ring_data = {[this](f32 ring_data) { OnRingConUpdate(ring_data); }},
.on_amiibo_data = {[this, port, type](const Joycon::TagInfo& tag_info) {
OnAmiiboUpdate(port, type, tag_info);
}},
.on_camera_data = {[this, port](const std::vector<u8>& camera_data,
Joycon::IrsResolution format) {
OnCameraUpdate(port, camera_data, format);
}},
};
handle->InitializeDevice();
handle->SetCallbacks(callbacks);
}
}
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetNextFreeHandle(
Joycon::ControllerType type) const {
if (type == Joycon::ControllerType::Left) {
const auto unconnected_device =
std::ranges::find_if(left_joycons, [](auto& device) { return !device->IsConnected(); });
if (unconnected_device != left_joycons.end()) {
return *unconnected_device;
}
}
if (type == Joycon::ControllerType::Right) {
const auto unconnected_device = std::ranges::find_if(
right_joycons, [](auto& device) { return !device->IsConnected(); });
if (unconnected_device != right_joycons.end()) {
return *unconnected_device;
}
}
if (type == Joycon::ControllerType::Pro) {
const auto unconnected_device = std::ranges::find_if(
pro_controller, [](auto& device) { return !device->IsConnected(); });
if (unconnected_device != pro_controller.end()) {
return *unconnected_device;
}
}
return nullptr;
}
bool Joycons::IsVibrationEnabled(const PadIdentifier& identifier) {
const auto handle = GetHandle(identifier);
if (handle == nullptr) {
return false;
}
return handle->IsVibrationEnabled();
}
Common::Input::DriverResult Joycons::SetVibration(const PadIdentifier& identifier,
const Common::Input::VibrationStatus& vibration) {
const Joycon::VibrationValue native_vibration{
.low_amplitude = vibration.low_amplitude,
.low_frequency = vibration.low_frequency,
.high_amplitude = vibration.high_amplitude,
.high_frequency = vibration.high_frequency,
};
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::DriverResult::InvalidHandle;
}
handle->SetVibration(native_vibration);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult Joycons::SetLeds(const PadIdentifier& identifier,
const Common::Input::LedStatus& led_status) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::DriverResult::InvalidHandle;
}
int led_config = led_status.led_1 ? 1 : 0;
led_config += led_status.led_2 ? 2 : 0;
led_config += led_status.led_3 ? 4 : 0;
led_config += led_status.led_4 ? 8 : 0;
return handle->SetLedConfig(static_cast<u8>(led_config));
}
Common::Input::DriverResult Joycons::SetCameraFormat(const PadIdentifier& identifier,
Common::Input::CameraFormat camera_format) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::DriverResult::InvalidHandle;
}
return handle->SetIrsConfig(Joycon::IrsMode::ImageTransfer,
static_cast<Joycon::IrsResolution>(camera_format));
};
Common::Input::NfcState Joycons::SupportsNfc(const PadIdentifier& identifier_) const {
return Common::Input::NfcState::Success;
};
Common::Input::NfcState Joycons::StartNfcPolling(const PadIdentifier& identifier) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::NfcState::Unknown;
}
return TranslateDriverResult(handle->StartNfcPolling());
};
Common::Input::NfcState Joycons::StopNfcPolling(const PadIdentifier& identifier) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::NfcState::Unknown;
}
return TranslateDriverResult(handle->StopNfcPolling());
};
Common::Input::NfcState Joycons::ReadAmiiboData(const PadIdentifier& identifier,
std::vector<u8>& out_data) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::NfcState::Unknown;
}
return TranslateDriverResult(handle->ReadAmiiboData(out_data));
}
Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier,
const std::vector<u8>& data) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::NfcState::Unknown;
}
return TranslateDriverResult(handle->WriteNfcData(data));
};
Common::Input::NfcState Joycons::ReadMifareData(const PadIdentifier& identifier,
const Common::Input::MifareRequest& request,
Common::Input::MifareRequest& data) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::NfcState::Unknown;
}
const auto command = static_cast<Joycon::MifareCmd>(request.data[0].command);
std::vector<Joycon::MifareReadChunk> read_request{};
for (const auto& request_data : request.data) {
if (request_data.command == 0) {
continue;
}
Joycon::MifareReadChunk chunk = {
.command = command,
.sector_key = {},
.sector = request_data.sector,
};
memcpy(chunk.sector_key.data(), request_data.key.data(),
sizeof(Joycon::MifareReadChunk::sector_key));
read_request.emplace_back(chunk);
}
std::vector<Joycon::MifareReadData> read_data(read_request.size());
const auto result = handle->ReadMifareData(read_request, read_data);
if (result == Common::Input::DriverResult::Success) {
for (std::size_t i = 0; i < read_request.size(); i++) {
data.data[i] = {
.command = static_cast<u8>(command),
.sector = read_data[i].sector,
.key = {},
.data = read_data[i].data,
};
}
}
return TranslateDriverResult(result);
};
Common::Input::NfcState Joycons::WriteMifareData(const PadIdentifier& identifier,
const Common::Input::MifareRequest& request) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::NfcState::Unknown;
}
const auto command = static_cast<Joycon::MifareCmd>(request.data[0].command);
std::vector<Joycon::MifareWriteChunk> write_request{};
for (const auto& request_data : request.data) {
if (request_data.command == 0) {
continue;
}
Joycon::MifareWriteChunk chunk = {
.command = command,
.sector_key = {},
.sector = request_data.sector,
.data = {},
};
memcpy(chunk.sector_key.data(), request_data.key.data(),
sizeof(Joycon::MifareReadChunk::sector_key));
memcpy(chunk.data.data(), request_data.data.data(), sizeof(Joycon::MifareWriteChunk::data));
write_request.emplace_back(chunk);
}
return TranslateDriverResult(handle->WriteMifareData(write_request));
};
Common::Input::DriverResult Joycons::SetPollingMode(const PadIdentifier& identifier,
const Common::Input::PollingMode polling_mode) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
LOG_ERROR(Input, "Invalid handle {}", identifier.port);
return Common::Input::DriverResult::InvalidHandle;
}
switch (polling_mode) {
case Common::Input::PollingMode::Active:
return handle->SetActiveMode();
case Common::Input::PollingMode::Passive:
return handle->SetPassiveMode();
case Common::Input::PollingMode::IR:
return handle->SetIrMode();
case Common::Input::PollingMode::NFC:
return handle->SetNfcMode();
case Common::Input::PollingMode::Ring:
return handle->SetRingConMode();
default:
return Common::Input::DriverResult::NotSupported;
}
}
void Joycons::OnBatteryUpdate(std::size_t port, Joycon::ControllerType type,
Joycon::Battery value) {
const auto identifier = GetIdentifier(port, type);
if (value.charging != 0) {
SetBattery(identifier, Common::Input::BatteryLevel::Charging);
return;
}
Common::Input::BatteryLevel battery{};
switch (value.status) {
case 0:
battery = Common::Input::BatteryLevel::Empty;
break;
case 1:
battery = Common::Input::BatteryLevel::Critical;
break;
case 2:
battery = Common::Input::BatteryLevel::Low;
break;
case 3:
battery = Common::Input::BatteryLevel::Medium;
break;
case 4:
default:
battery = Common::Input::BatteryLevel::Full;
break;
}
SetBattery(identifier, battery);
}
void Joycons::OnColorUpdate(std::size_t port, Joycon::ControllerType type,
const Joycon::Color& value) {
const auto identifier = GetIdentifier(port, type);
Common::Input::BodyColorStatus color{
.body = value.body,
.buttons = value.buttons,
.left_grip = value.left_grip,
.right_grip = value.right_grip,
};
SetColor(identifier, color);
}
void Joycons::OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value) {
const auto identifier = GetIdentifier(port, type);
SetButton(identifier, id, value);
}
void Joycons::OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value) {
const auto identifier = GetIdentifier(port, type);
SetAxis(identifier, id, value);
}
void Joycons::OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
const Joycon::MotionData& value) {
const auto identifier = GetIdentifier(port, type);
BasicMotion motion_data{
.gyro_x = value.gyro_x,
.gyro_y = value.gyro_y,
.gyro_z = value.gyro_z,
.accel_x = value.accel_x,
.accel_y = value.accel_y,
.accel_z = value.accel_z,
.delta_timestamp = 15000,
};
SetMotion(identifier, id, motion_data);
}
void Joycons::OnRingConUpdate(f32 ring_data) {
// To simplify ring detection it will always be mapped to an empty identifier for all
// controllers
static constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
SetAxis(identifier, 100, ring_data);
}
void Joycons::OnAmiiboUpdate(std::size_t port, Joycon::ControllerType type,
const Joycon::TagInfo& tag_info) {
const auto identifier = GetIdentifier(port, type);
const auto nfc_state = tag_info.uuid_length == 0 ? Common::Input::NfcState::AmiiboRemoved
: Common::Input::NfcState::NewAmiibo;
const Common::Input::NfcStatus nfc_status{
.state = nfc_state,
.uuid_length = tag_info.uuid_length,
.protocol = tag_info.protocol,
.tag_type = tag_info.tag_type,
.uuid = tag_info.uuid,
};
SetNfc(identifier, nfc_status);
}
void Joycons::OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data,
Joycon::IrsResolution format) {
const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right);
SetCamera(identifier, {static_cast<Common::Input::CameraFormat>(format), camera_data});
}
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetHandle(PadIdentifier identifier) const {
auto is_handle_active = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
if (!device) {
return false;
}
if (!device->IsConnected()) {
return false;
}
if (device->GetDevicePort() == identifier.port) {
return true;
}
return false;
};
const auto type = static_cast<Joycon::ControllerType>(identifier.pad);
if (type == Joycon::ControllerType::Left) {
const auto matching_device = std::ranges::find_if(
left_joycons, [is_handle_active](auto& device) { return is_handle_active(device); });
if (matching_device != left_joycons.end()) {
return *matching_device;
}
}
if (type == Joycon::ControllerType::Right) {
const auto matching_device = std::ranges::find_if(
right_joycons, [is_handle_active](auto& device) { return is_handle_active(device); });
if (matching_device != right_joycons.end()) {
return *matching_device;
}
}
if (type == Joycon::ControllerType::Pro) {
const auto matching_device = std::ranges::find_if(
pro_controller, [is_handle_active](auto& device) { return is_handle_active(device); });
if (matching_device != pro_controller.end()) {
return *matching_device;
}
}
return nullptr;
}
PadIdentifier Joycons::GetIdentifier(std::size_t port, Joycon::ControllerType type) const {
const std::array<u8, 16> guid{0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, static_cast<u8>(type)};
return {
.guid = Common::UUID{guid},
.port = port,
.pad = static_cast<std::size_t>(type),
};
}
Common::ParamPackage Joycons::GetParamPackage(std::size_t port, Joycon::ControllerType type) const {
const auto identifier = GetIdentifier(port, type);
return {
{"engine", GetEngineName()},
{"guid", identifier.guid.RawString()},
{"port", std::to_string(identifier.port)},
{"pad", std::to_string(identifier.pad)},
};
}
std::vector<Common::ParamPackage> Joycons::GetInputDevices() const {
std::vector<Common::ParamPackage> devices{};
auto add_entry = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
if (!device) {
return;
}
if (!device->IsConnected()) {
return;
}
auto param = GetParamPackage(device->GetDevicePort(), device->GetHandleDeviceType());
std::string name = fmt::format("{} {}", JoyconName(device->GetHandleDeviceType()),
device->GetDevicePort() + 1);
param.Set("display", std::move(name));
devices.emplace_back(param);
};
for (const auto& controller : left_joycons) {
add_entry(controller);
}
for (const auto& controller : right_joycons) {
add_entry(controller);
}
for (const auto& controller : pro_controller) {
add_entry(controller);
}
// List dual joycon pairs
for (std::size_t i = 0; i < MaxSupportedControllers; i++) {
if (!left_joycons[i] || !right_joycons[i]) {
continue;
}
if (!left_joycons[i]->IsConnected() || !right_joycons[i]->IsConnected()) {
continue;
}
auto main_param = GetParamPackage(i, left_joycons[i]->GetHandleDeviceType());
const auto second_param = GetParamPackage(i, right_joycons[i]->GetHandleDeviceType());
const auto type = Joycon::ControllerType::Dual;
std::string name = fmt::format("{} {}", JoyconName(type), i + 1);
main_param.Set("display", std::move(name));
main_param.Set("guid2", second_param.Get("guid", ""));
main_param.Set("pad", std::to_string(static_cast<size_t>(type)));
devices.emplace_back(main_param);
}
return devices;
}
ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) {
static constexpr std::array<std::tuple<Settings::NativeButton::Values, Joycon::PadButton, bool>,
18>
switch_to_joycon_button = {
std::tuple{Settings::NativeButton::A, Joycon::PadButton::A, true},
{Settings::NativeButton::B, Joycon::PadButton::B, true},
{Settings::NativeButton::X, Joycon::PadButton::X, true},
{Settings::NativeButton::Y, Joycon::PadButton::Y, true},
{Settings::NativeButton::DLeft, Joycon::PadButton::Left, false},
{Settings::NativeButton::DUp, Joycon::PadButton::Up, false},
{Settings::NativeButton::DRight, Joycon::PadButton::Right, false},
{Settings::NativeButton::DDown, Joycon::PadButton::Down, false},
{Settings::NativeButton::L, Joycon::PadButton::L, false},
{Settings::NativeButton::R, Joycon::PadButton::R, true},
{Settings::NativeButton::ZL, Joycon::PadButton::ZL, false},
{Settings::NativeButton::ZR, Joycon::PadButton::ZR, true},
{Settings::NativeButton::Plus, Joycon::PadButton::Plus, true},
{Settings::NativeButton::Minus, Joycon::PadButton::Minus, false},
{Settings::NativeButton::Home, Joycon::PadButton::Home, true},
{Settings::NativeButton::Screenshot, Joycon::PadButton::Capture, false},
{Settings::NativeButton::LStick, Joycon::PadButton::StickL, false},
{Settings::NativeButton::RStick, Joycon::PadButton::StickR, true},
};
if (!params.Has("port")) {
return {};
}
ButtonMapping mapping{};
for (const auto& [switch_button, joycon_button, side] : switch_to_joycon_button) {
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
auto pad = static_cast<Joycon::ControllerType>(params.Get("pad", 0));
if (pad == Joycon::ControllerType::Dual) {
pad = side ? Joycon::ControllerType::Right : Joycon::ControllerType::Left;
}
Common::ParamPackage button_params = GetParamPackage(port, pad);
button_params.Set("button", static_cast<int>(joycon_button));
mapping.insert_or_assign(switch_button, std::move(button_params));
}
// Map SL and SR buttons for left joycons
if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Left)) {
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
Common::ParamPackage button_params = GetParamPackage(port, Joycon::ControllerType::Left);
Common::ParamPackage sl_button_params = button_params;
Common::ParamPackage sr_button_params = button_params;
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSL));
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSR));
mapping.insert_or_assign(Settings::NativeButton::SLLeft, std::move(sl_button_params));
mapping.insert_or_assign(Settings::NativeButton::SRLeft, std::move(sr_button_params));
}
// Map SL and SR buttons for right joycons
if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Right)) {
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
Common::ParamPackage button_params = GetParamPackage(port, Joycon::ControllerType::Right);
Common::ParamPackage sl_button_params = button_params;
Common::ParamPackage sr_button_params = button_params;
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSL));
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSR));
mapping.insert_or_assign(Settings::NativeButton::SLRight, std::move(sl_button_params));
mapping.insert_or_assign(Settings::NativeButton::SRRight, std::move(sr_button_params));
}
return mapping;
}
AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return {};
}
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
auto pad_left = static_cast<Joycon::ControllerType>(params.Get("pad", 0));
auto pad_right = pad_left;
if (pad_left == Joycon::ControllerType::Dual) {
pad_left = Joycon::ControllerType::Left;
pad_right = Joycon::ControllerType::Right;
}
AnalogMapping mapping = {};
Common::ParamPackage left_analog_params = GetParamPackage(port, pad_left);
left_analog_params.Set("axis_x", static_cast<int>(Joycon::PadAxes::LeftStickX));
left_analog_params.Set("axis_y", static_cast<int>(Joycon::PadAxes::LeftStickY));
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
Common::ParamPackage right_analog_params = GetParamPackage(port, pad_right);
right_analog_params.Set("axis_x", static_cast<int>(Joycon::PadAxes::RightStickX));
right_analog_params.Set("axis_y", static_cast<int>(Joycon::PadAxes::RightStickY));
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
return mapping;
}
MotionMapping Joycons::GetMotionMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return {};
}
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
auto pad_left = static_cast<Joycon::ControllerType>(params.Get("pad", 0));
auto pad_right = pad_left;
if (pad_left == Joycon::ControllerType::Dual) {
pad_left = Joycon::ControllerType::Left;
pad_right = Joycon::ControllerType::Right;
}
MotionMapping mapping = {};
Common::ParamPackage left_motion_params = GetParamPackage(port, pad_left);
left_motion_params.Set("motion", 0);
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params));
Common::ParamPackage right_Motion_params = GetParamPackage(port, pad_right);
right_Motion_params.Set("motion", 1);
mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_Motion_params));
return mapping;
}
Common::Input::ButtonNames Joycons::GetUIButtonName(const Common::ParamPackage& params) const {
const auto button = static_cast<Joycon::PadButton>(params.Get("button", 0));
switch (button) {
case Joycon::PadButton::Left:
return Common::Input::ButtonNames::ButtonLeft;
case Joycon::PadButton::Right:
return Common::Input::ButtonNames::ButtonRight;
case Joycon::PadButton::Down:
return Common::Input::ButtonNames::ButtonDown;
case Joycon::PadButton::Up:
return Common::Input::ButtonNames::ButtonUp;
case Joycon::PadButton::LeftSL:
case Joycon::PadButton::RightSL:
return Common::Input::ButtonNames::TriggerSL;
case Joycon::PadButton::LeftSR:
case Joycon::PadButton::RightSR:
return Common::Input::ButtonNames::TriggerSR;
case Joycon::PadButton::L:
return Common::Input::ButtonNames::TriggerL;
case Joycon::PadButton::R:
return Common::Input::ButtonNames::TriggerR;
case Joycon::PadButton::ZL:
return Common::Input::ButtonNames::TriggerZL;
case Joycon::PadButton::ZR:
return Common::Input::ButtonNames::TriggerZR;
case Joycon::PadButton::A:
return Common::Input::ButtonNames::ButtonA;
case Joycon::PadButton::B:
return Common::Input::ButtonNames::ButtonB;
case Joycon::PadButton::X:
return Common::Input::ButtonNames::ButtonX;
case Joycon::PadButton::Y:
return Common::Input::ButtonNames::ButtonY;
case Joycon::PadButton::Plus:
return Common::Input::ButtonNames::ButtonPlus;
case Joycon::PadButton::Minus:
return Common::Input::ButtonNames::ButtonMinus;
case Joycon::PadButton::Home:
return Common::Input::ButtonNames::ButtonHome;
case Joycon::PadButton::Capture:
return Common::Input::ButtonNames::ButtonCapture;
case Joycon::PadButton::StickL:
return Common::Input::ButtonNames::ButtonStickL;
case Joycon::PadButton::StickR:
return Common::Input::ButtonNames::ButtonStickR;
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames Joycons::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("motion")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
std::string Joycons::JoyconName(Joycon::ControllerType type) const {
switch (type) {
case Joycon::ControllerType::Left:
return "Left Joycon";
case Joycon::ControllerType::Right:
return "Right Joycon";
case Joycon::ControllerType::Pro:
return "Pro Controller";
case Joycon::ControllerType::Dual:
return "Dual Joycon";
default:
return "Unknown Switch Controller";
}
}
Common::Input::NfcState Joycons::TranslateDriverResult(Common::Input::DriverResult result) const {
switch (result) {
case Common::Input::DriverResult::Success:
return Common::Input::NfcState::Success;
case Common::Input::DriverResult::Disabled:
return Common::Input::NfcState::WrongDeviceState;
case Common::Input::DriverResult::NotSupported:
return Common::Input::NfcState::NotSupported;
default:
return Common::Input::NfcState::Unknown;
}
}
} // namespace InputCommon

View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <span>
#include <thread>
#include <SDL_hidapi.h>
#include "input_common/input_engine.h"
namespace InputCommon::Joycon {
using SerialNumber = std::array<u8, 15>;
struct Battery;
struct Color;
struct MotionData;
struct TagInfo;
enum class ControllerType : u8;
enum class IrsResolution;
class JoyconDriver;
} // namespace InputCommon::Joycon
namespace InputCommon {
class Joycons final : public InputCommon::InputEngine {
public:
explicit Joycons(const std::string& input_engine_);
~Joycons();
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
Common::Input::DriverResult SetLeds(const PadIdentifier& identifier,
const Common::Input::LedStatus& led_status) override;
Common::Input::DriverResult SetCameraFormat(const PadIdentifier& identifier,
Common::Input::CameraFormat camera_format) override;
Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier) const override;
Common::Input::NfcState StartNfcPolling(const PadIdentifier& identifier) override;
Common::Input::NfcState StopNfcPolling(const PadIdentifier& identifier) override;
Common::Input::NfcState ReadAmiiboData(const PadIdentifier& identifier,
std::vector<u8>& out_data) override;
Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier,
const std::vector<u8>& data) override;
Common::Input::NfcState ReadMifareData(const PadIdentifier& identifier,
const Common::Input::MifareRequest& request,
Common::Input::MifareRequest& out_data) override;
Common::Input::NfcState WriteMifareData(const PadIdentifier& identifier,
const Common::Input::MifareRequest& request) override;
Common::Input::DriverResult SetPollingMode(
const PadIdentifier& identifier, const Common::Input::PollingMode polling_mode) override;
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
private:
static constexpr std::size_t MaxSupportedControllers = 8;
/// For shutting down, clear all data, join all threads, release usb devices
void Reset();
/// Registers controllers, clears all data and starts the scan thread
void Setup();
/// Actively searches for new devices
void ScanThread(std::stop_token stop_token);
/// Returns true if device is valid and not registered
bool IsDeviceNew(SDL_hid_device_info* device_info) const;
/// Tries to connect to the new device
void RegisterNewDevice(SDL_hid_device_info* device_info);
/// Returns the next free handle
std::shared_ptr<Joycon::JoyconDriver> GetNextFreeHandle(Joycon::ControllerType type) const;
void OnBatteryUpdate(std::size_t port, Joycon::ControllerType type, Joycon::Battery value);
void OnColorUpdate(std::size_t port, Joycon::ControllerType type, const Joycon::Color& value);
void OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value);
void OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value);
void OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
const Joycon::MotionData& value);
void OnRingConUpdate(f32 ring_data);
void OnAmiiboUpdate(std::size_t port, Joycon::ControllerType type,
const Joycon::TagInfo& amiibo_data);
void OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data,
Joycon::IrsResolution format);
/// Returns a JoyconHandle corresponding to a PadIdentifier
std::shared_ptr<Joycon::JoyconDriver> GetHandle(PadIdentifier identifier) const;
/// Returns a PadIdentifier corresponding to the port number and joycon type
PadIdentifier GetIdentifier(std::size_t port, Joycon::ControllerType type) const;
/// Returns a ParamPackage corresponding to the port number and joycon type
Common::ParamPackage GetParamPackage(std::size_t port, Joycon::ControllerType type) const;
std::string JoyconName(std::size_t port) const;
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
/// Returns the name of the device in text format
std::string JoyconName(Joycon::ControllerType type) const;
Common::Input::NfcState TranslateDriverResult(Common::Input::DriverResult result) const;
std::jthread scan_thread;
// Joycon types are split by type to ease supporting dualjoycon configurations
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> left_joycons{};
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> right_joycons{};
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> pro_controller{};
};
} // namespace InputCommon

View File

@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/param_package.h"
#include "common/settings_input.h"
#include "input_common/drivers/keyboard.h"
namespace InputCommon {
constexpr PadIdentifier key_identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
constexpr PadIdentifier keyboard_key_identifier = {
.guid = Common::UUID{},
.port = 1,
.pad = 0,
};
constexpr PadIdentifier keyboard_modifier_identifier = {
.guid = Common::UUID{},
.port = 1,
.pad = 1,
};
Keyboard::Keyboard(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
// Keyboard is broken into 3 different sets:
// key: Unfiltered intended for controllers.
// keyboard_key: Allows only Settings::NativeKeyboard::Keys intended for keyboard emulation.
// keyboard_modifier: Allows only Settings::NativeKeyboard::Modifiers intended for keyboard
// emulation.
PreSetController(key_identifier);
PreSetController(keyboard_key_identifier);
PreSetController(keyboard_modifier_identifier);
}
void Keyboard::PressKey(int key_code) {
SetButton(key_identifier, key_code, true);
}
void Keyboard::ReleaseKey(int key_code) {
SetButton(key_identifier, key_code, false);
}
void Keyboard::PressKeyboardKey(int key_index) {
if (key_index == Settings::NativeKeyboard::None) {
return;
}
SetButton(keyboard_key_identifier, key_index, true);
}
void Keyboard::ReleaseKeyboardKey(int key_index) {
if (key_index == Settings::NativeKeyboard::None) {
return;
}
SetButton(keyboard_key_identifier, key_index, false);
}
void Keyboard::SetKeyboardModifiers(int key_modifiers) {
for (int i = 0; i < 32; ++i) {
bool key_value = ((key_modifiers >> i) & 0x1) != 0;
SetButton(keyboard_modifier_identifier, i, key_value);
// Use the modifier to press the key button equivalent
switch (i) {
case Settings::NativeKeyboard::LeftControl:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftControlKey, key_value);
break;
case Settings::NativeKeyboard::LeftShift:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftShiftKey, key_value);
break;
case Settings::NativeKeyboard::LeftAlt:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftAltKey, key_value);
break;
case Settings::NativeKeyboard::LeftMeta:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftMetaKey, key_value);
break;
case Settings::NativeKeyboard::RightControl:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightControlKey,
key_value);
break;
case Settings::NativeKeyboard::RightShift:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightShiftKey, key_value);
break;
case Settings::NativeKeyboard::RightAlt:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightAltKey, key_value);
break;
case Settings::NativeKeyboard::RightMeta:
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightMetaKey, key_value);
break;
default:
// Other modifier keys should be pressed with PressKey since they stay enabled until
// next press
break;
}
}
}
void Keyboard::ReleaseAllKeys() {
ResetButtonState();
}
std::vector<Common::ParamPackage> Keyboard::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", "Keyboard Only"},
});
return devices;
}
} // namespace InputCommon

View File

@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Keyboard final : public InputEngine {
public:
explicit Keyboard(std::string input_engine_);
/**
* Sets the status of all buttons bound with the key to pressed
* @param key_code the code of the key to press
*/
void PressKey(int key_code);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void ReleaseKey(int key_code);
/**
* Sets the status of the keyboard key to pressed
* @param key_index index of the key to press
*/
void PressKeyboardKey(int key_index);
/**
* Sets the status of the keyboard key to released
* @param key_index index of the key to release
*/
void ReleaseKeyboardKey(int key_index);
/**
* Sets the status of all keyboard modifier keys
* @param key_modifiers the code of the key to release
*/
void SetKeyboardModifiers(int key_modifiers);
/// Sets all keys to the non pressed state
void ReleaseAllKeys();
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
};
} // namespace InputCommon

View File

@@ -0,0 +1,316 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#include <fmt/ranges.h>
#include <math.h>
#include "common/param_package.h"
#include "common/settings.h"
#include "common/thread.h"
#include "input_common/drivers/mouse.h"
namespace InputCommon {
constexpr int update_time = 10;
constexpr float default_panning_sensitivity = 0.0010f;
constexpr float default_stick_sensitivity = 0.0006f;
constexpr float default_deadzone_counterweight = 0.01f;
constexpr float default_motion_panning_sensitivity = 2.5f;
constexpr float default_motion_sensitivity = 0.416f;
constexpr float maximum_rotation_speed = 2.0f;
constexpr float maximum_stick_range = 1.5f;
constexpr int mouse_axis_x = 0;
constexpr int mouse_axis_y = 1;
constexpr int wheel_axis_x = 2;
constexpr int wheel_axis_y = 3;
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
constexpr PadIdentifier motion_identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 1,
};
constexpr PadIdentifier real_mouse_identifier = {
.guid = Common::UUID{},
.port = 1,
.pad = 0,
};
constexpr PadIdentifier touch_identifier = {
.guid = Common::UUID{},
.port = 2,
.pad = 0,
};
Mouse::Mouse(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(identifier);
PreSetController(real_mouse_identifier);
PreSetController(touch_identifier);
PreSetController(motion_identifier);
// Initialize all mouse axis
PreSetAxis(identifier, mouse_axis_x);
PreSetAxis(identifier, mouse_axis_y);
PreSetAxis(identifier, wheel_axis_x);
PreSetAxis(identifier, wheel_axis_y);
PreSetAxis(real_mouse_identifier, mouse_axis_x);
PreSetAxis(real_mouse_identifier, mouse_axis_y);
PreSetAxis(touch_identifier, mouse_axis_x);
PreSetAxis(touch_identifier, mouse_axis_y);
// Initialize variables
mouse_origin = {};
last_mouse_position = {};
wheel_position = {};
last_mouse_change = {};
last_motion_change = {};
update_thread = std::jthread([this](std::stop_token stop_token) { UpdateThread(stop_token); });
}
void Mouse::UpdateThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("Mouse");
while (!stop_token.stop_requested()) {
UpdateStickInput();
UpdateMotionInput();
std::this_thread::sleep_for(std::chrono::milliseconds(update_time));
}
}
void Mouse::UpdateStickInput() {
if (!IsMousePanningEnabled()) {
return;
}
const float length = last_mouse_change.Length();
// Prevent input from exceeding the max range (1.0f) too much,
// but allow some room to make it easier to sustain
if (length > maximum_stick_range) {
last_mouse_change /= length;
last_mouse_change *= maximum_stick_range;
}
SetAxis(identifier, mouse_axis_x, last_mouse_change.x);
SetAxis(identifier, mouse_axis_y, -last_mouse_change.y);
// Decay input over time
const float clamped_length = std::min(1.0f, length);
const float decay_strength = Settings::values.mouse_panning_decay_strength.GetValue();
const float decay = 1 - clamped_length * clamped_length * decay_strength * 0.01f;
const float min_decay = Settings::values.mouse_panning_min_decay.GetValue();
const float clamped_decay = std::min(1 - min_decay / 100.0f, decay);
last_mouse_change *= clamped_decay;
}
void Mouse::UpdateMotionInput() {
const float sensitivity =
IsMousePanningEnabled() ? default_motion_panning_sensitivity : default_motion_sensitivity;
const float rotation_velocity = std::sqrt(last_motion_change.x * last_motion_change.x +
last_motion_change.y * last_motion_change.y);
// Clamp rotation speed
if (rotation_velocity > maximum_rotation_speed / sensitivity) {
const float multiplier = maximum_rotation_speed / rotation_velocity / sensitivity;
last_motion_change.x = last_motion_change.x * multiplier;
last_motion_change.y = last_motion_change.y * multiplier;
}
const BasicMotion motion_data{
.gyro_x = last_motion_change.x * sensitivity,
.gyro_y = last_motion_change.y * sensitivity,
.gyro_z = last_motion_change.z * sensitivity,
.accel_x = 0,
.accel_y = 0,
.accel_z = 0,
.delta_timestamp = update_time * 1000,
};
if (IsMousePanningEnabled()) {
last_motion_change.x = 0;
last_motion_change.y = 0;
}
last_motion_change.z = 0;
SetMotion(motion_identifier, 0, motion_data);
}
void Mouse::Move(int x, int y, int center_x, int center_y) {
if (IsMousePanningEnabled()) {
const auto mouse_change =
(Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
const float x_sensitivity =
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_panning_sensitivity;
const float y_sensitivity =
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_panning_sensitivity;
const float deadzone_counterweight =
Settings::values.mouse_panning_deadzone_counterweight.GetValue() *
default_deadzone_counterweight;
last_motion_change += {-mouse_change.y * x_sensitivity, -mouse_change.x * y_sensitivity, 0};
last_mouse_change.x += mouse_change.x * x_sensitivity;
last_mouse_change.y += mouse_change.y * y_sensitivity;
// Bind the mouse change to [0 <= deadzone_counterweight <= 1.0]
const float length = last_mouse_change.Length();
if (length < deadzone_counterweight && length != 0.0f) {
last_mouse_change /= length;
last_mouse_change *= deadzone_counterweight;
}
return;
}
if (button_pressed) {
const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
const float x_sensitivity =
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_stick_sensitivity;
const float y_sensitivity =
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_stick_sensitivity;
SetAxis(identifier, mouse_axis_x, static_cast<float>(mouse_move.x) * x_sensitivity);
SetAxis(identifier, mouse_axis_y, static_cast<float>(-mouse_move.y) * y_sensitivity);
last_motion_change = {
static_cast<float>(-mouse_move.y) * x_sensitivity,
static_cast<float>(-mouse_move.x) * y_sensitivity,
last_motion_change.z,
};
}
}
void Mouse::MouseMove(f32 touch_x, f32 touch_y) {
SetAxis(real_mouse_identifier, mouse_axis_x, touch_x);
SetAxis(real_mouse_identifier, mouse_axis_y, touch_y);
}
void Mouse::TouchMove(f32 touch_x, f32 touch_y) {
SetAxis(touch_identifier, mouse_axis_x, touch_x);
SetAxis(touch_identifier, mouse_axis_y, touch_y);
}
void Mouse::PressButton(int x, int y, MouseButton button) {
SetButton(identifier, static_cast<int>(button), true);
// Set initial analog parameters
mouse_origin = {x, y};
last_mouse_position = {x, y};
button_pressed = true;
}
void Mouse::PressMouseButton(MouseButton button) {
SetButton(real_mouse_identifier, static_cast<int>(button), true);
}
void Mouse::PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button) {
SetAxis(touch_identifier, mouse_axis_x, touch_x);
SetAxis(touch_identifier, mouse_axis_y, touch_y);
SetButton(touch_identifier, static_cast<int>(button), true);
}
void Mouse::ReleaseButton(MouseButton button) {
SetButton(identifier, static_cast<int>(button), false);
SetButton(real_mouse_identifier, static_cast<int>(button), false);
SetButton(touch_identifier, static_cast<int>(button), false);
if (!IsMousePanningEnabled()) {
SetAxis(identifier, mouse_axis_x, 0);
SetAxis(identifier, mouse_axis_y, 0);
}
last_motion_change.x = 0;
last_motion_change.y = 0;
button_pressed = false;
}
void Mouse::MouseWheelChange(int x, int y) {
wheel_position.x += x;
wheel_position.y += y;
last_motion_change.z += static_cast<f32>(y);
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position.x));
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position.y));
}
void Mouse::ReleaseAllButtons() {
ResetButtonState();
button_pressed = false;
}
bool Mouse::IsMousePanningEnabled() {
// Disable mouse panning when a real mouse is connected
return Settings::values.mouse_panning && !Settings::values.mouse_enabled;
}
std::vector<Common::ParamPackage> Mouse::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", "Keyboard/Mouse"},
});
return devices;
}
AnalogMapping Mouse::GetAnalogMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
// Only overwrite different buttons from default
AnalogMapping mapping = {};
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("axis_x", 0);
right_analog_params.Set("axis_y", 1);
right_analog_params.Set("threshold", 0.5f);
right_analog_params.Set("range", 1.0f);
right_analog_params.Set("deadzone", 0.0f);
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
return mapping;
}
Common::Input::ButtonNames Mouse::GetUIButtonName(const Common::ParamPackage& params) const {
const auto button = static_cast<MouseButton>(params.Get("button", 0));
switch (button) {
case MouseButton::Left:
return Common::Input::ButtonNames::ButtonLeft;
case MouseButton::Right:
return Common::Input::ButtonNames::ButtonRight;
case MouseButton::Wheel:
return Common::Input::ButtonNames::ButtonMouseWheel;
case MouseButton::Backward:
return Common::Input::ButtonNames::ButtonBackward;
case MouseButton::Forward:
return Common::Input::ButtonNames::ButtonForward;
case MouseButton::Task:
return Common::Input::ButtonNames::ButtonTask;
case MouseButton::Extra:
return Common::Input::ButtonNames::ButtonExtra;
case MouseButton::Undefined:
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames Mouse::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
return Common::Input::ButtonNames::Engine;
}
if (params.Has("motion")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
} // namespace InputCommon

View File

@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <thread>
#include "common/polyfill_thread.h"
#include "common/vector_math.h"
#include "input_common/input_engine.h"
namespace InputCommon {
enum class MouseButton {
Left,
Right,
Wheel,
Backward,
Forward,
Task,
Extra,
Undefined,
};
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Mouse final : public InputEngine {
public:
explicit Mouse(std::string input_engine_);
/**
* Signals that mouse has moved.
* @param x the x-coordinate of the cursor
* @param y the y-coordinate of the cursor
* @param center_x the x-coordinate of the middle of the screen
* @param center_y the y-coordinate of the middle of the screen
*/
void Move(int x, int y, int center_x, int center_y);
/**
* Signals that real mouse has moved.
* @param x the absolute position on the touchscreen of the cursor
* @param y the absolute position on the touchscreen of the cursor
*/
void MouseMove(f32 touch_x, f32 touch_y);
/**
* Signals that touch finger has moved.
* @param x the absolute position on the touchscreen of the cursor
* @param y the absolute position on the touchscreen of the cursor
*/
void TouchMove(f32 touch_x, f32 touch_y);
/**
* Sets the status of a button to pressed
* @param x the x-coordinate of the cursor
* @param y the y-coordinate of the cursor
* @param button the id of the button to press
*/
void PressButton(int x, int y, MouseButton button);
/**
* Sets the status of a mouse button to pressed
* @param button the id of the button to press
*/
void PressMouseButton(MouseButton button);
/**
* Sets the status of touch finger to pressed
* @param x the absolute position on the touchscreen of the cursor
* @param y the absolute position on the touchscreen of the cursor
* @param button the id of the button to press
*/
void PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void ReleaseButton(MouseButton button);
/**
* Sets the status of the mouse wheel
* @param x delta movement in the x direction
* @param y delta movement in the y direction
*/
void MouseWheelChange(int x, int y);
void ReleaseAllButtons();
std::vector<Common::ParamPackage> GetInputDevices() const override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
private:
void UpdateThread(std::stop_token stop_token);
void UpdateStickInput();
void UpdateMotionInput();
bool IsMousePanningEnabled();
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
Common::Vec2<int> mouse_origin;
Common::Vec2<int> last_mouse_position;
Common::Vec2<float> last_mouse_change;
Common::Vec3<float> last_motion_change;
Common::Vec2<int> wheel_position;
bool button_pressed;
std::jthread update_thread;
};
} // namespace InputCommon

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <SDL.h>
#include "common/common_types.h"
#include "common/threadsafe_queue.h"
#include "input_common/input_engine.h"
union SDL_Event;
using SDL_GameController = struct _SDL_GameController;
using SDL_Joystick = struct _SDL_Joystick;
using SDL_JoystickID = s32;
namespace InputCommon {
class SDLJoystick;
using ButtonBindings =
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 20>;
using ZButtonBindings =
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>;
class SDLDriver : public InputEngine {
public:
/// Initializes and registers SDL device factories
explicit SDLDriver(std::string input_engine_);
/// Unregisters SDL device factories and shut them down.
~SDLDriver() override;
void PumpEvents() const;
/// Handle SDL_Events for joysticks from SDL_PollEvent
void HandleGameControllerEvent(const SDL_Event& event);
/// Get the nth joystick with the corresponding GUID
std::shared_ptr<SDLJoystick> GetSDLJoystickBySDLID(SDL_JoystickID sdl_id);
/**
* Check how many identical joysticks (by guid) were connected before the one with sdl_id and so
* tie it to a SDLJoystick with the same guid and that port
*/
std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const Common::UUID& guid, int port);
std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const std::string& guid, int port);
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
std::string GetHatButtonName(u8 direction_value) const override;
u8 GetHatButtonId(const std::string& direction_name) const override;
bool IsStickInverted(const Common::ParamPackage& params) override;
Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
private:
void InitJoystick(int joystick_index);
void CloseJoystick(SDL_Joystick* sdl_joystick);
/// Needs to be called before SDL_QuitSubSystem.
void CloseJoysticks();
/// Takes all vibrations from the queue and sends the command to the controller
void SendVibrations();
Common::ParamPackage BuildAnalogParamPackageForButton(int port, const Common::UUID& guid,
s32 axis, float value = 0.1f) const;
Common::ParamPackage BuildButtonParamPackageForButton(int port, const Common::UUID& guid,
s32 button) const;
Common::ParamPackage BuildHatParamPackageForButton(int port, const Common::UUID& guid, s32 hat,
u8 value) const;
Common::ParamPackage BuildMotionParam(int port, const Common::UUID& guid) const;
Common::ParamPackage BuildParamPackageForBinding(
int port, const Common::UUID& guid, const SDL_GameControllerButtonBind& binding) const;
Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y, float offset_x,
float offset_y) const;
/// Returns the default button bindings list
ButtonBindings GetDefaultButtonBinding(const std::shared_ptr<SDLJoystick>& joystick) const;
/// Returns the button mappings from a single controller
ButtonMapping GetSingleControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const;
/// Returns the button mappings from two different controllers
ButtonMapping GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const std::shared_ptr<SDLJoystick>& joystick2,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const;
/// Returns true if the button is on the left joycon
bool IsButtonOnLeftSide(Settings::NativeButton::Values button) const;
/// Queue of vibration request to controllers
Common::SPSCQueue<VibrationRequest> vibration_queue;
/// Map of GUID of a list of corresponding virtual Joysticks
std::unordered_map<Common::UUID, std::vector<std::shared_ptr<SDLJoystick>>> joystick_map;
std::mutex joystick_map_mutex;
bool start_thread = false;
std::atomic<bool> initialized = false;
std::thread vibration_thread;
};
} // namespace InputCommon

View File

@@ -0,0 +1,344 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <sstream>
#include <fmt/ranges.h>
#include "common/fs/file.h"
#include "common/fs/fs_types.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "input_common/drivers/tas_input.h"
namespace InputCommon::TasInput {
enum class Tas::TasAxis : u8 {
StickX,
StickY,
SubstickX,
SubstickY,
Undefined,
};
// Supported keywords and buttons from a TAS file
constexpr std::array<std::pair<std::string_view, TasButton>, 18> text_to_tas_button = {
std::pair{"KEY_A", TasButton::BUTTON_A},
{"KEY_B", TasButton::BUTTON_B},
{"KEY_X", TasButton::BUTTON_X},
{"KEY_Y", TasButton::BUTTON_Y},
{"KEY_LSTICK", TasButton::STICK_L},
{"KEY_RSTICK", TasButton::STICK_R},
{"KEY_L", TasButton::TRIGGER_L},
{"KEY_R", TasButton::TRIGGER_R},
{"KEY_PLUS", TasButton::BUTTON_PLUS},
{"KEY_MINUS", TasButton::BUTTON_MINUS},
{"KEY_DLEFT", TasButton::BUTTON_LEFT},
{"KEY_DUP", TasButton::BUTTON_UP},
{"KEY_DRIGHT", TasButton::BUTTON_RIGHT},
{"KEY_DDOWN", TasButton::BUTTON_DOWN},
{"KEY_SL", TasButton::BUTTON_SL},
{"KEY_SR", TasButton::BUTTON_SR},
// These buttons are disabled to avoid TAS input from activating hotkeys
// {"KEY_CAPTURE", TasButton::BUTTON_CAPTURE},
// {"KEY_HOME", TasButton::BUTTON_HOME},
{"KEY_ZL", TasButton::TRIGGER_ZL},
{"KEY_ZR", TasButton::TRIGGER_ZR},
};
Tas::Tas(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
for (size_t player_index = 0; player_index < PLAYER_NUMBER; player_index++) {
PadIdentifier identifier{
.guid = Common::UUID{},
.port = player_index,
.pad = 0,
};
PreSetController(identifier);
}
ClearInput();
if (!Settings::values.tas_enable) {
needs_reset = true;
return;
}
LoadTasFiles();
}
Tas::~Tas() {
Stop();
}
void Tas::LoadTasFiles() {
script_length = 0;
for (size_t i = 0; i < commands.size(); i++) {
LoadTasFile(i, 0);
if (commands[i].size() > script_length) {
script_length = commands[i].size();
}
}
}
void Tas::LoadTasFile(size_t player_index, size_t file_index) {
commands[player_index].clear();
std::string file = Common::FS::ReadStringFromFile(
Common::FS::GetCitronPath(Common::FS::CitronPath::TASDir) /
fmt::format("script{}-{}.txt", file_index, player_index + 1),
Common::FS::FileType::BinaryFile);
std::istringstream command_line(file);
std::string line;
int frame_no = 0;
while (std::getline(command_line, line, '\n')) {
if (line.empty()) {
continue;
}
std::vector<std::string> seg_list;
{
std::istringstream line_stream(line);
std::string segment;
while (std::getline(line_stream, segment, ' ')) {
seg_list.push_back(std::move(segment));
}
}
if (seg_list.size() < 4) {
continue;
}
try {
const auto num_frames = std::stoi(seg_list[0]);
while (frame_no < num_frames) {
commands[player_index].emplace_back();
frame_no++;
}
} catch (const std::invalid_argument&) {
LOG_ERROR(Input, "Invalid argument: '{}' at command {}", seg_list[0], frame_no);
} catch (const std::out_of_range&) {
LOG_ERROR(Input, "Out of range: '{}' at command {}", seg_list[0], frame_no);
}
TASCommand command = {
.buttons = ReadCommandButtons(seg_list[1]),
.l_axis = ReadCommandAxis(seg_list[2]),
.r_axis = ReadCommandAxis(seg_list[3]),
};
commands[player_index].push_back(command);
frame_no++;
}
LOG_INFO(Input, "TAS file loaded! {} frames", frame_no);
}
void Tas::WriteTasFile(std::u8string_view file_name) {
std::string output_text;
for (size_t frame = 0; frame < record_commands.size(); frame++) {
const TASCommand& line = record_commands[frame];
output_text += fmt::format("{} {} {} {}\n", frame, WriteCommandButtons(line.buttons),
WriteCommandAxis(line.l_axis), WriteCommandAxis(line.r_axis));
}
const auto tas_file_name = Common::FS::GetCitronPath(Common::FS::CitronPath::TASDir) / file_name;
const auto bytes_written =
Common::FS::WriteStringToFile(tas_file_name, Common::FS::FileType::TextFile, output_text);
if (bytes_written == output_text.size()) {
LOG_INFO(Input, "TAS file written to file!");
} else {
LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written,
output_text.size());
}
}
void Tas::RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis) {
last_input = {
.buttons = buttons,
.l_axis = left_axis,
.r_axis = right_axis,
};
}
std::tuple<TasState, size_t, std::array<size_t, PLAYER_NUMBER>> Tas::GetStatus() const {
TasState state;
std::array<size_t, PLAYER_NUMBER> lengths{0};
if (is_recording) {
lengths[0] = record_commands.size();
return {TasState::Recording, record_commands.size(), lengths};
}
if (is_running) {
state = TasState::Running;
} else {
state = TasState::Stopped;
}
for (size_t i = 0; i < PLAYER_NUMBER; i++) {
lengths[i] = commands[i].size();
}
return {state, current_command, lengths};
}
void Tas::UpdateThread() {
if (!Settings::values.tas_enable) {
if (is_running) {
Stop();
}
return;
}
if (is_recording) {
record_commands.push_back(last_input);
}
if (needs_reset) {
current_command = 0;
needs_reset = false;
LoadTasFiles();
LOG_DEBUG(Input, "tas_reset done");
}
if (!is_running) {
ClearInput();
return;
}
if (current_command < script_length) {
LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length);
const size_t frame = current_command++;
for (size_t player_index = 0; player_index < commands.size(); player_index++) {
TASCommand command{};
if (frame < commands[player_index].size()) {
command = commands[player_index][frame];
}
PadIdentifier identifier{
.guid = Common::UUID{},
.port = player_index,
.pad = 0,
};
for (std::size_t i = 0; i < sizeof(command.buttons) * 8; ++i) {
const bool button_status = (command.buttons & (1LLU << i)) != 0;
const int button = static_cast<int>(i);
SetButton(identifier, button, button_status);
}
SetTasAxis(identifier, TasAxis::StickX, command.l_axis.x);
SetTasAxis(identifier, TasAxis::StickY, command.l_axis.y);
SetTasAxis(identifier, TasAxis::SubstickX, command.r_axis.x);
SetTasAxis(identifier, TasAxis::SubstickY, command.r_axis.y);
}
} else {
is_running = Settings::values.tas_loop.GetValue();
LoadTasFiles();
current_command = 0;
ClearInput();
}
}
void Tas::ClearInput() {
ResetButtonState();
ResetAnalogState();
}
TasAnalog Tas::ReadCommandAxis(const std::string& line) const {
std::vector<std::string> seg_list;
{
std::istringstream line_stream(line);
std::string segment;
while (std::getline(line_stream, segment, ';')) {
seg_list.push_back(std::move(segment));
}
}
if (seg_list.size() < 2) {
LOG_ERROR(Input, "Invalid axis data: '{}'", line);
return {};
}
try {
const float x = std::stof(seg_list.at(0)) / 32767.0f;
const float y = std::stof(seg_list.at(1)) / 32767.0f;
return {x, y};
} catch (const std::invalid_argument&) {
LOG_ERROR(Input, "Invalid argument: '{}'", line);
} catch (const std::out_of_range&) {
LOG_ERROR(Input, "Out of range: '{}'", line);
}
return {};
}
u64 Tas::ReadCommandButtons(const std::string& line) const {
std::istringstream button_text(line);
std::string button_line;
u64 buttons = 0;
while (std::getline(button_text, button_line, ';')) {
for (const auto& [text, tas_button] : text_to_tas_button) {
if (text == button_line) {
buttons |= static_cast<u64>(tas_button);
break;
}
}
}
return buttons;
}
std::string Tas::WriteCommandButtons(u64 buttons) const {
std::string returns;
for (const auto& [text_button, tas_button] : text_to_tas_button) {
if ((buttons & static_cast<u64>(tas_button)) != 0) {
returns += fmt::format("{};", text_button);
}
}
return returns.empty() ? "NONE" : returns;
}
std::string Tas::WriteCommandAxis(TasAnalog analog) const {
return fmt::format("{};{}", analog.x * 32767, analog.y * 32767);
}
void Tas::SetTasAxis(const PadIdentifier& identifier, TasAxis axis, f32 value) {
SetAxis(identifier, static_cast<int>(axis), value);
}
void Tas::StartStop() {
if (!Settings::values.tas_enable) {
return;
}
if (is_running) {
Stop();
} else {
is_running = true;
}
}
void Tas::Stop() {
is_running = false;
}
void Tas::Reset() {
if (!Settings::values.tas_enable) {
return;
}
needs_reset = true;
}
bool Tas::Record() {
if (!Settings::values.tas_enable) {
return true;
}
is_recording = !is_recording;
return is_recording;
}
void Tas::SaveRecording(bool overwrite_file) {
if (is_recording) {
return;
}
if (record_commands.empty()) {
return;
}
WriteTasFile(u8"record.txt");
if (overwrite_file) {
WriteTasFile(u8"script0-1.txt");
}
needs_reset = true;
record_commands.clear();
}
} // namespace InputCommon::TasInput

View File

@@ -0,0 +1,200 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <string>
#include <vector>
#include "common/common_types.h"
#include "input_common/input_engine.h"
/*
To play back TAS scripts on Citron, select the folder with scripts in the configuration menu below
Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt
for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players).
A script file has the same format as TAS-nx uses, so final files will look like this:
1 KEY_B 0;0 0;0
6 KEY_ZL 0;0 0;0
41 KEY_ZL;KEY_Y 0;0 0;0
43 KEY_X;KEY_A 32767;0 0;0
44 KEY_A 32767;0 0;0
45 KEY_A 32767;0 0;0
46 KEY_A 32767;0 0;0
47 KEY_A 32767;0 0;0
After placing the file at the correct location, it can be read into Citron with the (default) hotkey
CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file
has. Playback can be started or stopped using CTRL+F5.
However, for playback to actually work, the correct input device has to be selected: In the Controls
menu, select TAS from the device list for the controller that the script should be played on.
Recording a new script file is really simple: Just make sure that the proper device (not TAS) is
connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke
again (CTRL+F7). The new script will be saved at the location previously selected, as the filename
record.txt.
For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller
P1).
*/
namespace InputCommon::TasInput {
constexpr size_t PLAYER_NUMBER = 10;
enum class TasButton : u64 {
BUTTON_A = 1U << 0,
BUTTON_B = 1U << 1,
BUTTON_X = 1U << 2,
BUTTON_Y = 1U << 3,
STICK_L = 1U << 4,
STICK_R = 1U << 5,
TRIGGER_L = 1U << 6,
TRIGGER_R = 1U << 7,
TRIGGER_ZL = 1U << 8,
TRIGGER_ZR = 1U << 9,
BUTTON_PLUS = 1U << 10,
BUTTON_MINUS = 1U << 11,
BUTTON_LEFT = 1U << 12,
BUTTON_UP = 1U << 13,
BUTTON_RIGHT = 1U << 14,
BUTTON_DOWN = 1U << 15,
BUTTON_SL = 1U << 16,
BUTTON_SR = 1U << 17,
BUTTON_HOME = 1U << 18,
BUTTON_CAPTURE = 1U << 19,
};
struct TasAnalog {
float x{};
float y{};
};
enum class TasState {
Running,
Recording,
Stopped,
};
class Tas final : public InputEngine {
public:
explicit Tas(std::string input_engine_);
~Tas() override;
/**
* Changes the input status that will be stored in each frame
* @param buttons Bitfield with the status of the buttons
* @param left_axis Value of the left axis
* @param right_axis Value of the right axis
*/
void RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis);
// Main loop that records or executes input
void UpdateThread();
// Sets the flag to start or stop the TAS command execution and swaps controllers profiles
void StartStop();
// Stop the TAS and reverts any controller profile
void Stop();
// Sets the flag to reload the file and start from the beginning in the next update
void Reset();
/**
* Sets the flag to enable or disable recording of inputs
* @returns true if the current recording status is enabled
*/
bool Record();
/**
* Saves contents of record_commands on a file
* @param overwrite_file Indicates if player 1 should be overwritten
*/
void SaveRecording(bool overwrite_file);
/**
* Returns the current status values of TAS playback/recording
* @returns A Tuple of
* TasState indicating the current state out of Running ;
* Current playback progress ;
* Total length of script file currently loaded or being recorded
*/
std::tuple<TasState, size_t, std::array<size_t, PLAYER_NUMBER>> GetStatus() const;
private:
enum class TasAxis : u8;
struct TASCommand {
u64 buttons{};
TasAnalog l_axis{};
TasAnalog r_axis{};
};
/// Loads TAS files from all players
void LoadTasFiles();
/**
* Loads TAS file from the specified player
* @param player_index Player number to save the script
* @param file_index Script number of the file
*/
void LoadTasFile(size_t player_index, size_t file_index);
/**
* Writes a TAS file from the recorded commands
* @param file_name Name of the file to be written
*/
void WriteTasFile(std::u8string_view file_name);
/**
* Parses a string containing the axis values. X and Y have a range from -32767 to 32767
* @param line String containing axis values with the following format "x;y"
* @returns A TAS analog object with axis values with range from -1.0 to 1.0
*/
TasAnalog ReadCommandAxis(const std::string& line) const;
/**
* Parses a string containing the button values. Each button is represented by it's text format
* specified in text_to_tas_button array
* @param line string containing button name with the following format "a;b;c;d..."
* @returns A u64 with each bit representing the status of a button
*/
u64 ReadCommandButtons(const std::string& line) const;
/**
* Reset state of all players
*/
void ClearInput();
/**
* Converts an u64 containing the button status into the text equivalent
* @param buttons Bitfield with the status of the buttons
* @returns A string with the name of the buttons to be written to the file
*/
std::string WriteCommandButtons(u64 buttons) const;
/**
* Converts an TAS analog object containing the axis status into the text equivalent
* @param analog Value of the axis
* @returns A string with the value of the axis to be written to the file
*/
std::string WriteCommandAxis(TasAnalog analog) const;
/// Sets an axis for a particular pad to the given value.
void SetTasAxis(const PadIdentifier& identifier, TasAxis axis, f32 value);
size_t script_length{0};
bool is_recording{false};
bool is_running{false};
bool needs_reset{false};
std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{};
std::vector<TASCommand> record_commands{};
size_t current_command{0};
TASCommand last_input{}; // only used for recording
};
} // namespace InputCommon::TasInput

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/param_package.h"
#include "input_common/drivers/touch_screen.h"
namespace InputCommon {
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
TouchScreen::TouchScreen(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(identifier);
ReleaseAllTouch();
}
void TouchScreen::TouchMoved(float x, float y, std::size_t finger_id) {
const auto index = GetIndexFromFingerId(finger_id);
if (!index) {
// Touch doesn't exist handle it as a new one
TouchPressed(x, y, finger_id);
return;
}
const auto i = index.value();
fingers[i].is_active = true;
SetButton(identifier, static_cast<int>(i), true);
SetAxis(identifier, static_cast<int>(i * 2), x);
SetAxis(identifier, static_cast<int>(i * 2 + 1), y);
}
void TouchScreen::TouchPressed(float x, float y, std::size_t finger_id) {
if (GetIndexFromFingerId(finger_id)) {
// Touch already exist. Just update the data
TouchMoved(x, y, finger_id);
return;
}
const auto index = GetNextFreeIndex();
if (!index) {
// No free entries. Ignore input
return;
}
const auto i = index.value();
fingers[i].is_enabled = true;
fingers[i].finger_id = finger_id;
TouchMoved(x, y, finger_id);
}
void TouchScreen::TouchReleased(std::size_t finger_id) {
const auto index = GetIndexFromFingerId(finger_id);
if (!index) {
return;
}
const auto i = index.value();
fingers[i].is_enabled = false;
SetButton(identifier, static_cast<int>(i), false);
SetAxis(identifier, static_cast<int>(i * 2), 0.0f);
SetAxis(identifier, static_cast<int>(i * 2 + 1), 0.0f);
}
std::optional<std::size_t> TouchScreen::GetIndexFromFingerId(std::size_t finger_id) const {
for (std::size_t index = 0; index < MAX_FINGER_COUNT; ++index) {
const auto& finger = fingers[index];
if (!finger.is_enabled) {
continue;
}
if (finger.finger_id == finger_id) {
return index;
}
}
return std::nullopt;
}
std::optional<std::size_t> TouchScreen::GetNextFreeIndex() const {
for (std::size_t index = 0; index < MAX_FINGER_COUNT; ++index) {
if (!fingers[index].is_enabled) {
return index;
}
}
return std::nullopt;
}
void TouchScreen::ClearActiveFlag() {
for (auto& finger : fingers) {
finger.is_active = false;
}
}
void TouchScreen::ReleaseInactiveTouch() {
for (const auto& finger : fingers) {
if (!finger.is_active) {
TouchReleased(finger.finger_id);
}
}
}
void TouchScreen::ReleaseAllTouch() {
for (const auto& finger : fingers) {
if (finger.is_enabled) {
TouchReleased(finger.finger_id);
}
}
}
} // namespace InputCommon

View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A touch device factory representing a touch screen. It receives touch events and forward them
* to all touch devices it created.
*/
class TouchScreen final : public InputEngine {
public:
explicit TouchScreen(std::string input_engine_);
/**
* Signals that touch has moved and marks this touch point as active
* @param x new horizontal position
* @param y new vertical position
* @param finger_id of the touch point to be updated
*/
void TouchMoved(float x, float y, std::size_t finger_id);
/**
* Signals and creates a new touch point with this finger id
* @param x starting horizontal position
* @param y starting vertical position
* @param finger_id to be assigned to the new touch point
*/
void TouchPressed(float x, float y, std::size_t finger_id);
/**
* Signals and resets the touch point related to the this finger id
* @param finger_id to be released
*/
void TouchReleased(std::size_t finger_id);
/// Resets the active flag for each touch point
void ClearActiveFlag();
/// Releases all touch that haven't been marked as active
void ReleaseInactiveTouch();
/// Resets all inputs to their initial value
void ReleaseAllTouch();
private:
static constexpr std::size_t MAX_FINGER_COUNT = 16;
struct TouchStatus {
std::size_t finger_id{};
bool is_enabled{};
bool is_active{};
};
std::optional<std::size_t> GetIndexFromFingerId(std::size_t finger_id) const;
std::optional<std::size_t> GetNextFreeIndex() const;
std::array<TouchStatus, MAX_FINGER_COUNT> fingers{};
};
} // namespace InputCommon

View File

@@ -0,0 +1,653 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <random>
#include <boost/asio.hpp>
#include <fmt/ranges.h>
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/helpers/udp_protocol.h"
using boost::asio::ip::udp;
namespace InputCommon::CemuhookUDP {
struct SocketCallback {
std::function<void(Response::Version)> version;
std::function<void(Response::PortInfo)> port_info;
std::function<void(Response::PadData)> pad_data;
};
class Socket {
public:
using clock = std::chrono::system_clock;
explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
: callback(std::move(callback_)), timer(io_service),
socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
boost::system::error_code ec{};
auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
if (ec.value() != boost::system::errc::success) {
LOG_ERROR(Input, "Invalid IPv4 address \"{}\" provided to socket", host);
ipv4 = boost::asio::ip::address_v4{};
}
send_endpoint = {udp::endpoint(ipv4, port)};
}
void Stop() {
io_service.stop();
}
void Loop() {
io_service.run();
}
void StartSend(const clock::time_point& from) {
timer.expires_at(from + std::chrono::seconds(3));
timer.async_wait([this](const boost::system::error_code& error) { HandleSend(error); });
}
void StartReceive() {
socket.async_receive_from(
boost::asio::buffer(receive_buffer), receive_endpoint,
[this](const boost::system::error_code& error, std::size_t bytes_transferred) {
HandleReceive(error, bytes_transferred);
});
}
private:
u32 GenerateRandomClientId() const {
std::random_device device;
return device();
}
void HandleReceive(const boost::system::error_code&, std::size_t bytes_transferred) {
if (auto type = Response::Validate(receive_buffer.data(), bytes_transferred)) {
switch (*type) {
case Type::Version: {
Response::Version version;
std::memcpy(&version, &receive_buffer[sizeof(Header)], sizeof(Response::Version));
callback.version(std::move(version));
break;
}
case Type::PortInfo: {
Response::PortInfo port_info;
std::memcpy(&port_info, &receive_buffer[sizeof(Header)],
sizeof(Response::PortInfo));
callback.port_info(std::move(port_info));
break;
}
case Type::PadData: {
Response::PadData pad_data;
std::memcpy(&pad_data, &receive_buffer[sizeof(Header)], sizeof(Response::PadData));
callback.pad_data(std::move(pad_data));
break;
}
}
}
StartReceive();
}
void HandleSend(const boost::system::error_code&) {
boost::system::error_code _ignored{};
// Send a request for getting port info for the pad
const Request::PortInfo port_info{4, {0, 1, 2, 3}};
const auto port_message = Request::Create(port_info, client_id);
std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE);
socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint, {}, _ignored);
// Send a request for getting pad data for the pad
const Request::PadData pad_data{
Request::RegisterFlags::AllPads,
0,
EMPTY_MAC_ADDRESS,
};
const auto pad_message = Request::Create(pad_data, client_id);
std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE);
socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint, {}, _ignored);
StartSend(timer.expiry());
}
SocketCallback callback;
boost::asio::io_service io_service;
boost::asio::basic_waitable_timer<clock> timer;
udp::socket socket;
const u32 client_id;
static constexpr std::size_t PORT_INFO_SIZE = sizeof(Message<Request::PortInfo>);
static constexpr std::size_t PAD_DATA_SIZE = sizeof(Message<Request::PadData>);
std::array<u8, PORT_INFO_SIZE> send_buffer1;
std::array<u8, PAD_DATA_SIZE> send_buffer2;
udp::endpoint send_endpoint;
std::array<u8, MAX_PACKET_SIZE> receive_buffer;
udp::endpoint receive_endpoint;
};
static void SocketLoop(Socket* socket) {
socket->StartReceive();
socket->StartSend(Socket::clock::now());
socket->Loop();
}
UDPClient::UDPClient(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
LOG_INFO(Input, "Udp Initialization started");
ReloadSockets();
}
UDPClient::~UDPClient() {
Reset();
}
UDPClient::ClientConnection::ClientConnection() = default;
UDPClient::ClientConnection::~ClientConnection() = default;
void UDPClient::ReloadSockets() {
Reset();
std::stringstream servers_ss(Settings::values.udp_input_servers.GetValue());
std::string server_token;
std::size_t client = 0;
while (std::getline(servers_ss, server_token, ',')) {
if (client == MAX_UDP_CLIENTS) {
break;
}
std::stringstream server_ss(server_token);
std::string token;
std::getline(server_ss, token, ':');
std::string udp_input_address = token;
std::getline(server_ss, token, ':');
char* temp;
const u16 udp_input_port = static_cast<u16>(std::strtol(token.c_str(), &temp, 0));
if (*temp != '\0') {
LOG_ERROR(Input, "Port number is not valid {}", token);
continue;
}
const std::size_t client_number = GetClientNumber(udp_input_address, udp_input_port);
if (client_number != MAX_UDP_CLIENTS) {
LOG_ERROR(Input, "Duplicated UDP servers found");
continue;
}
StartCommunication(client++, udp_input_address, udp_input_port);
}
}
std::size_t UDPClient::GetClientNumber(std::string_view host, u16 port) const {
for (std::size_t client = 0; client < clients.size(); client++) {
if (clients[client].active == -1) {
continue;
}
if (clients[client].host == host && clients[client].port == port) {
return client;
}
}
return MAX_UDP_CLIENTS;
}
Common::Input::BatteryLevel UDPClient::GetBatteryLevel(Response::Battery battery) const {
switch (battery) {
case Response::Battery::Dying:
return Common::Input::BatteryLevel::Empty;
case Response::Battery::Low:
return Common::Input::BatteryLevel::Critical;
case Response::Battery::Medium:
return Common::Input::BatteryLevel::Low;
case Response::Battery::High:
return Common::Input::BatteryLevel::Medium;
case Response::Battery::Full:
case Response::Battery::Charged:
return Common::Input::BatteryLevel::Full;
case Response::Battery::Charging:
default:
return Common::Input::BatteryLevel::Charging;
}
}
void UDPClient::OnVersion([[maybe_unused]] Response::Version data) {
LOG_TRACE(Input, "Version packet received: {}", data.version);
}
void UDPClient::OnPortInfo([[maybe_unused]] Response::PortInfo data) {
LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
}
void UDPClient::OnPadData(Response::PadData data, std::size_t client) {
const std::size_t pad_index = (client * PADS_PER_CLIENT) + data.info.id;
if (pad_index >= pads.size()) {
LOG_ERROR(Input, "Invalid pad id {}", data.info.id);
return;
}
LOG_TRACE(Input, "PadData packet received");
if (data.packet_counter == pads[pad_index].packet_sequence) {
LOG_WARNING(
Input,
"PadData packet dropped because its stale info. Current count: {} Packet count: {}",
pads[pad_index].packet_sequence, data.packet_counter);
pads[pad_index].connected = false;
return;
}
clients[client].active = 1;
pads[pad_index].connected = true;
pads[pad_index].packet_sequence = data.packet_counter;
const auto now = std::chrono::steady_clock::now();
const auto time_difference = static_cast<u64>(
std::chrono::duration_cast<std::chrono::microseconds>(now - pads[pad_index].last_update)
.count());
pads[pad_index].last_update = now;
// Gyroscope values are not it the correct scale from better joy.
// Dividing by 312 allows us to make one full turn = 1 turn
// This must be a configurable valued called sensitivity
const float gyro_scale = 1.0f / 312.0f;
const BasicMotion motion{
.gyro_x = data.gyro.pitch * gyro_scale,
.gyro_y = data.gyro.roll * gyro_scale,
.gyro_z = -data.gyro.yaw * gyro_scale,
.accel_x = data.accel.x,
.accel_y = -data.accel.z,
.accel_z = data.accel.y,
.delta_timestamp = time_difference,
};
const PadIdentifier identifier = GetPadIdentifier(pad_index);
SetMotion(identifier, 0, motion);
for (std::size_t id = 0; id < data.touch.size(); ++id) {
const auto touch_pad = data.touch[id];
const auto touch_axis_x_id =
static_cast<int>(id == 0 ? PadAxes::Touch1X : PadAxes::Touch2X);
const auto touch_axis_y_id =
static_cast<int>(id == 0 ? PadAxes::Touch1Y : PadAxes::Touch2Y);
const auto touch_button_id =
static_cast<int>(id == 0 ? PadButton::Touch1 : PadButton::Touch2);
// TODO: Use custom calibration per device
const Common::ParamPackage touch_param(Settings::values.touch_device.GetValue());
const u16 min_x = static_cast<u16>(touch_param.Get("min_x", 100));
const u16 min_y = static_cast<u16>(touch_param.Get("min_y", 50));
const u16 max_x = static_cast<u16>(touch_param.Get("max_x", 1800));
const u16 max_y = static_cast<u16>(touch_param.Get("max_y", 850));
const f32 x =
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.x), min_x, max_x) - min_x) /
static_cast<f32>(max_x - min_x);
const f32 y =
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.y), min_y, max_y) - min_y) /
static_cast<f32>(max_y - min_y);
if (touch_pad.is_active) {
SetAxis(identifier, touch_axis_x_id, x);
SetAxis(identifier, touch_axis_y_id, y);
SetButton(identifier, touch_button_id, true);
continue;
}
SetAxis(identifier, touch_axis_x_id, 0);
SetAxis(identifier, touch_axis_y_id, 0);
SetButton(identifier, touch_button_id, false);
}
SetAxis(identifier, static_cast<int>(PadAxes::LeftStickX),
(data.left_stick_x - 127.0f) / 127.0f);
SetAxis(identifier, static_cast<int>(PadAxes::LeftStickY),
(data.left_stick_y - 127.0f) / 127.0f);
SetAxis(identifier, static_cast<int>(PadAxes::RightStickX),
(data.right_stick_x - 127.0f) / 127.0f);
SetAxis(identifier, static_cast<int>(PadAxes::RightStickY),
(data.right_stick_y - 127.0f) / 127.0f);
static constexpr std::array<PadButton, 16> buttons{
PadButton::Share, PadButton::L3, PadButton::R3, PadButton::Options,
PadButton::Up, PadButton::Right, PadButton::Down, PadButton::Left,
PadButton::L2, PadButton::R2, PadButton::L1, PadButton::R1,
PadButton::Triangle, PadButton::Circle, PadButton::Cross, PadButton::Square};
for (std::size_t i = 0; i < buttons.size(); ++i) {
const bool button_status = (data.digital_button & (1U << i)) != 0;
const int button = static_cast<int>(buttons[i]);
SetButton(identifier, button, button_status);
}
SetButton(identifier, static_cast<int>(PadButton::Home), data.home != 0);
SetButton(identifier, static_cast<int>(PadButton::TouchHardPress), data.touch_hard_press != 0);
SetBattery(identifier, GetBatteryLevel(data.info.battery));
}
void UDPClient::StartCommunication(std::size_t client, const std::string& host, u16 port) {
SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
[this](Response::PortInfo info) { OnPortInfo(info); },
[this, client](Response::PadData data) { OnPadData(data, client); }};
LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
clients[client].uuid = GetHostUUID(host);
clients[client].host = host;
clients[client].port = port;
clients[client].active = 0;
clients[client].socket = std::make_unique<Socket>(host, port, callback);
clients[client].thread = std::thread{SocketLoop, clients[client].socket.get()};
for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
const PadIdentifier identifier = GetPadIdentifier(client * PADS_PER_CLIENT + index);
PreSetController(identifier);
PreSetMotion(identifier, 0);
}
}
PadIdentifier UDPClient::GetPadIdentifier(std::size_t pad_index) const {
const std::size_t client = pad_index / PADS_PER_CLIENT;
return {
.guid = clients[client].uuid,
.port = static_cast<std::size_t>(clients[client].port),
.pad = pad_index,
};
}
Common::UUID UDPClient::GetHostUUID(const std::string& host) const {
const auto ip = boost::asio::ip::make_address_v4(host);
const auto hex_host = fmt::format("00000000-0000-0000-0000-0000{:06x}", ip.to_uint());
return Common::UUID{hex_host};
}
void UDPClient::Reset() {
for (auto& client : clients) {
if (client.thread.joinable()) {
client.active = -1;
client.socket->Stop();
client.thread.join();
}
}
}
std::vector<Common::ParamPackage> UDPClient::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
if (!Settings::values.enable_udp_controller) {
return devices;
}
for (std::size_t client = 0; client < clients.size(); client++) {
if (clients[client].active != 1) {
continue;
}
for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
const std::size_t pad_index = client * PADS_PER_CLIENT + index;
if (!pads[pad_index].connected) {
continue;
}
const auto pad_identifier = GetPadIdentifier(pad_index);
Common::ParamPackage identifier{};
identifier.Set("engine", GetEngineName());
identifier.Set("display", fmt::format("UDP Controller {}", pad_identifier.pad));
identifier.Set("guid", pad_identifier.guid.RawString());
identifier.Set("port", static_cast<int>(pad_identifier.port));
identifier.Set("pad", static_cast<int>(pad_identifier.pad));
devices.emplace_back(identifier);
}
}
return devices;
}
ButtonMapping UDPClient::GetButtonMappingForDevice(const Common::ParamPackage& params) {
// This list excludes any button that can't be really mapped
static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 22>
switch_to_dsu_button = {
std::pair{Settings::NativeButton::A, PadButton::Circle},
{Settings::NativeButton::B, PadButton::Cross},
{Settings::NativeButton::X, PadButton::Triangle},
{Settings::NativeButton::Y, PadButton::Square},
{Settings::NativeButton::Plus, PadButton::Options},
{Settings::NativeButton::Minus, PadButton::Share},
{Settings::NativeButton::DLeft, PadButton::Left},
{Settings::NativeButton::DUp, PadButton::Up},
{Settings::NativeButton::DRight, PadButton::Right},
{Settings::NativeButton::DDown, PadButton::Down},
{Settings::NativeButton::L, PadButton::L1},
{Settings::NativeButton::R, PadButton::R1},
{Settings::NativeButton::ZL, PadButton::L2},
{Settings::NativeButton::ZR, PadButton::R2},
{Settings::NativeButton::SLLeft, PadButton::L2},
{Settings::NativeButton::SRLeft, PadButton::R2},
{Settings::NativeButton::SLRight, PadButton::L2},
{Settings::NativeButton::SRRight, PadButton::R2},
{Settings::NativeButton::LStick, PadButton::L3},
{Settings::NativeButton::RStick, PadButton::R3},
{Settings::NativeButton::Home, PadButton::Home},
{Settings::NativeButton::Screenshot, PadButton::TouchHardPress},
};
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return {};
}
ButtonMapping mapping{};
for (const auto& [switch_button, dsu_button] : switch_to_dsu_button) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("guid", params.Get("guid", ""));
button_params.Set("port", params.Get("port", 0));
button_params.Set("pad", params.Get("pad", 0));
button_params.Set("button", static_cast<int>(dsu_button));
mapping.insert_or_assign(switch_button, std::move(button_params));
}
return mapping;
}
AnalogMapping UDPClient::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return {};
}
AnalogMapping mapping = {};
Common::ParamPackage left_analog_params;
left_analog_params.Set("engine", GetEngineName());
left_analog_params.Set("guid", params.Get("guid", ""));
left_analog_params.Set("port", params.Get("port", 0));
left_analog_params.Set("pad", params.Get("pad", 0));
left_analog_params.Set("axis_x", static_cast<int>(PadAxes::LeftStickX));
left_analog_params.Set("axis_y", static_cast<int>(PadAxes::LeftStickY));
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("guid", params.Get("guid", ""));
right_analog_params.Set("port", params.Get("port", 0));
right_analog_params.Set("pad", params.Get("pad", 0));
right_analog_params.Set("axis_x", static_cast<int>(PadAxes::RightStickX));
right_analog_params.Set("axis_y", static_cast<int>(PadAxes::RightStickY));
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
return mapping;
}
MotionMapping UDPClient::GetMotionMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return {};
}
MotionMapping mapping = {};
Common::ParamPackage left_motion_params;
left_motion_params.Set("engine", GetEngineName());
left_motion_params.Set("guid", params.Get("guid", ""));
left_motion_params.Set("port", params.Get("port", 0));
left_motion_params.Set("pad", params.Get("pad", 0));
left_motion_params.Set("motion", 0);
Common::ParamPackage right_motion_params;
right_motion_params.Set("engine", GetEngineName());
right_motion_params.Set("guid", params.Get("guid", ""));
right_motion_params.Set("port", params.Get("port", 0));
right_motion_params.Set("pad", params.Get("pad", 0));
right_motion_params.Set("motion", 0);
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params));
mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_motion_params));
return mapping;
}
Common::Input::ButtonNames UDPClient::GetUIButtonName(const Common::ParamPackage& params) const {
PadButton button = static_cast<PadButton>(params.Get("button", 0));
switch (button) {
case PadButton::Left:
return Common::Input::ButtonNames::ButtonLeft;
case PadButton::Right:
return Common::Input::ButtonNames::ButtonRight;
case PadButton::Down:
return Common::Input::ButtonNames::ButtonDown;
case PadButton::Up:
return Common::Input::ButtonNames::ButtonUp;
case PadButton::L1:
return Common::Input::ButtonNames::L1;
case PadButton::L2:
return Common::Input::ButtonNames::L2;
case PadButton::L3:
return Common::Input::ButtonNames::L3;
case PadButton::R1:
return Common::Input::ButtonNames::R1;
case PadButton::R2:
return Common::Input::ButtonNames::R2;
case PadButton::R3:
return Common::Input::ButtonNames::R3;
case PadButton::Circle:
return Common::Input::ButtonNames::Circle;
case PadButton::Cross:
return Common::Input::ButtonNames::Cross;
case PadButton::Square:
return Common::Input::ButtonNames::Square;
case PadButton::Triangle:
return Common::Input::ButtonNames::Triangle;
case PadButton::Share:
return Common::Input::ButtonNames::Share;
case PadButton::Options:
return Common::Input::ButtonNames::Options;
case PadButton::Home:
return Common::Input::ButtonNames::Home;
case PadButton::Touch1:
case PadButton::Touch2:
case PadButton::TouchHardPress:
return Common::Input::ButtonNames::Touch;
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames UDPClient::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("motion")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
bool UDPClient::IsStickInverted(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
return false;
}
const auto x_axis = static_cast<PadAxes>(params.Get("axis_x", 0));
const auto y_axis = static_cast<PadAxes>(params.Get("axis_y", 0));
if (x_axis != PadAxes::LeftStickY && x_axis != PadAxes::RightStickY) {
return false;
}
if (y_axis != PadAxes::LeftStickX && y_axis != PadAxes::RightStickX) {
return false;
}
return true;
}
void TestCommunication(const std::string& host, u16 port,
const std::function<void()>& success_callback,
const std::function<void()>& failure_callback) {
std::thread([=] {
Common::Event success_event;
SocketCallback callback{
.version = [](Response::Version) {},
.port_info = [](Response::PortInfo) {},
.pad_data = [&](Response::PadData) { success_event.Set(); },
};
Socket socket{host, port, std::move(callback)};
std::thread worker_thread{SocketLoop, &socket};
const bool result =
success_event.WaitUntil(std::chrono::steady_clock::now() + std::chrono::seconds(10));
socket.Stop();
worker_thread.join();
if (result) {
success_callback();
} else {
failure_callback();
}
}).detach();
}
CalibrationConfigurationJob::CalibrationConfigurationJob(
const std::string& host, u16 port, std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback) {
std::thread([=, this] {
u16 min_x{UINT16_MAX};
u16 min_y{UINT16_MAX};
u16 max_x{};
u16 max_y{};
Status current_status{Status::Initialized};
SocketCallback callback{[](Response::Version) {}, [](Response::PortInfo) {},
[&](Response::PadData data) {
constexpr u16 CALIBRATION_THRESHOLD = 100;
if (current_status == Status::Initialized) {
// Receiving data means the communication is ready now
current_status = Status::Ready;
status_callback(current_status);
}
if (data.touch[0].is_active == 0) {
return;
}
LOG_DEBUG(Input, "Current touch: {} {}", data.touch[0].x,
data.touch[0].y);
min_x = std::min(min_x, static_cast<u16>(data.touch[0].x));
min_y = std::min(min_y, static_cast<u16>(data.touch[0].y));
if (current_status == Status::Ready) {
// First touch - min data (min_x/min_y)
current_status = Status::Stage1Completed;
status_callback(current_status);
}
if (data.touch[0].x - min_x > CALIBRATION_THRESHOLD &&
data.touch[0].y - min_y > CALIBRATION_THRESHOLD) {
// Set the current position as max value and finishes
// configuration
max_x = data.touch[0].x;
max_y = data.touch[0].y;
current_status = Status::Completed;
data_callback(min_x, min_y, max_x, max_y);
status_callback(current_status);
complete_event.Set();
}
}};
Socket socket{host, port, std::move(callback)};
std::thread worker_thread{SocketLoop, &socket};
complete_event.Wait();
socket.Stop();
worker_thread.join();
}).detach();
}
CalibrationConfigurationJob::~CalibrationConfigurationJob() {
Stop();
}
void CalibrationConfigurationJob::Stop() {
complete_event.Set();
}
} // namespace InputCommon::CemuhookUDP

View File

@@ -0,0 +1,192 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include "common/common_types.h"
#include "common/thread.h"
#include "input_common/input_engine.h"
namespace InputCommon::CemuhookUDP {
class Socket;
namespace Response {
enum class Battery : u8;
struct PadData;
struct PortInfo;
struct TouchPad;
struct Version;
} // namespace Response
enum class PadTouch {
Click,
Undefined,
};
struct UDPPadStatus {
std::string host{"127.0.0.1"};
u16 port{26760};
std::size_t pad_index{};
};
struct DeviceStatus {
std::mutex update_mutex;
// calibration data for scaling the device's touch area to 3ds
struct CalibrationData {
u16 min_x{};
u16 min_y{};
u16 max_x{};
u16 max_y{};
};
std::optional<CalibrationData> touch_calibration;
};
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class UDPClient final : public InputEngine {
public:
explicit UDPClient(std::string input_engine_);
~UDPClient() override;
void ReloadSockets();
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
bool IsStickInverted(const Common::ParamPackage& params) override;
private:
enum class PadButton {
Undefined = 0x0000,
Share = 0x0001,
L3 = 0x0002,
R3 = 0x0004,
Options = 0x0008,
Up = 0x0010,
Right = 0x0020,
Down = 0x0040,
Left = 0x0080,
L2 = 0x0100,
R2 = 0x0200,
L1 = 0x0400,
R1 = 0x0800,
Triangle = 0x1000,
Circle = 0x2000,
Cross = 0x4000,
Square = 0x8000,
Touch1 = 0x10000,
Touch2 = 0x20000,
Home = 0x40000,
TouchHardPress = 0x80000,
};
enum class PadAxes : u8 {
LeftStickX,
LeftStickY,
RightStickX,
RightStickY,
AnalogLeft,
AnalogDown,
AnalogRight,
AnalogUp,
AnalogSquare,
AnalogCross,
AnalogCircle,
AnalogTriangle,
AnalogR1,
AnalogL1,
AnalogR2,
AnalogL3,
AnalogR3,
Touch1X,
Touch1Y,
Touch2X,
Touch2Y,
Undefined,
};
struct PadData {
std::size_t pad_index{};
bool connected{};
DeviceStatus status;
u64 packet_sequence{};
std::chrono::time_point<std::chrono::steady_clock> last_update;
};
struct ClientConnection {
ClientConnection();
~ClientConnection();
Common::UUID uuid{"00000000-0000-0000-0000-00007F000001"};
std::string host{"127.0.0.1"};
u16 port{26760};
s8 active{-1};
std::unique_ptr<Socket> socket;
std::thread thread;
};
// For shutting down, clear all data, join all threads, release usb
void Reset();
// Translates configuration to client number
std::size_t GetClientNumber(std::string_view host, u16 port) const;
// Translates UDP battery level to input engine battery level
Common::Input::BatteryLevel GetBatteryLevel(Response::Battery battery) const;
void OnVersion(Response::Version);
void OnPortInfo(Response::PortInfo);
void OnPadData(Response::PadData, std::size_t client);
void StartCommunication(std::size_t client, const std::string& host, u16 port);
PadIdentifier GetPadIdentifier(std::size_t pad_index) const;
Common::UUID GetHostUUID(const std::string& host) const;
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
// Allocate clients for 8 udp servers
static constexpr std::size_t MAX_UDP_CLIENTS = 8;
static constexpr std::size_t PADS_PER_CLIENT = 4;
std::array<PadData, MAX_UDP_CLIENTS * PADS_PER_CLIENT> pads{};
std::array<ClientConnection, MAX_UDP_CLIENTS> clients{};
};
/// An async job allowing configuration of the touchpad calibration.
class CalibrationConfigurationJob {
public:
enum class Status {
Initialized,
Ready,
Stage1Completed,
Completed,
};
/**
* Constructs and starts the job with the specified parameter.
*
* @param status_callback Callback for job status updates
* @param data_callback Called when calibration data is ready
*/
explicit CalibrationConfigurationJob(const std::string& host, u16 port,
std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback);
~CalibrationConfigurationJob();
void Stop();
private:
Common::Event complete_event;
};
void TestCommunication(const std::string& host, u16 port,
const std::function<void()>& success_callback,
const std::function<void()>& failure_callback);
} // namespace InputCommon::CemuhookUDP

View File

@@ -0,0 +1,257 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <cstring>
#include <fmt/ranges.h>
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "input_common/drivers/virtual_amiibo.h"
namespace InputCommon {
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
VirtualAmiibo::VirtualAmiibo(std::string input_engine_) : InputEngine(std::move(input_engine_)) {}
VirtualAmiibo::~VirtualAmiibo() = default;
Common::Input::DriverResult VirtualAmiibo::SetPollingMode(
[[maybe_unused]] const PadIdentifier& identifier_,
const Common::Input::PollingMode polling_mode_) {
polling_mode = polling_mode_;
switch (polling_mode) {
case Common::Input::PollingMode::NFC:
state = State::Initialized;
return Common::Input::DriverResult::Success;
default:
if (state == State::TagNearby) {
CloseAmiibo();
}
state = State::Disabled;
return Common::Input::DriverResult::NotSupported;
}
}
Common::Input::NfcState VirtualAmiibo::SupportsNfc(
[[maybe_unused]] const PadIdentifier& identifier_) const {
return Common::Input::NfcState::Success;
}
Common::Input::NfcState VirtualAmiibo::StartNfcPolling(const PadIdentifier& identifier_) {
if (state != State::Initialized) {
return Common::Input::NfcState::WrongDeviceState;
}
state = State::WaitingForAmiibo;
return Common::Input::NfcState::Success;
}
Common::Input::NfcState VirtualAmiibo::StopNfcPolling(const PadIdentifier& identifier_) {
if (state == State::Disabled) {
return Common::Input::NfcState::WrongDeviceState;
}
if (state == State::TagNearby) {
CloseAmiibo();
}
state = State::Initialized;
return Common::Input::NfcState::Success;
}
Common::Input::NfcState VirtualAmiibo::ReadAmiiboData(const PadIdentifier& identifier_,
std::vector<u8>& out_data) {
if (state != State::TagNearby) {
return Common::Input::NfcState::WrongDeviceState;
}
if (status.tag_type != 1U << 1) {
return Common::Input::NfcState::InvalidTagType;
}
out_data.resize(nfc_data.size());
memcpy(out_data.data(), nfc_data.data(), nfc_data.size());
return Common::Input::NfcState::Success;
}
Common::Input::NfcState VirtualAmiibo::WriteNfcData(
[[maybe_unused]] const PadIdentifier& identifier_, const std::vector<u8>& data) {
const Common::FS::IOFile nfc_file{file_path, Common::FS::FileAccessMode::ReadWrite,
Common::FS::FileType::BinaryFile};
if (!nfc_file.IsOpen()) {
LOG_ERROR(Core, "Amiibo is already on use");
return Common::Input::NfcState::WriteFailed;
}
if (!nfc_file.Write(data)) {
LOG_ERROR(Service_NFP, "Error writing to file");
return Common::Input::NfcState::WriteFailed;
}
nfc_data = data;
return Common::Input::NfcState::Success;
}
Common::Input::NfcState VirtualAmiibo::ReadMifareData(const PadIdentifier& identifier_,
const Common::Input::MifareRequest& request,
Common::Input::MifareRequest& out_data) {
if (state != State::TagNearby) {
return Common::Input::NfcState::WrongDeviceState;
}
if (status.tag_type != 1U << 6) {
return Common::Input::NfcState::InvalidTagType;
}
for (std::size_t i = 0; i < request.data.size(); i++) {
if (request.data[i].command == 0) {
continue;
}
out_data.data[i].command = request.data[i].command;
out_data.data[i].sector = request.data[i].sector;
const std::size_t sector_index =
request.data[i].sector * sizeof(Common::Input::MifareData::data);
if (nfc_data.size() < sector_index + sizeof(Common::Input::MifareData::data)) {
return Common::Input::NfcState::WriteFailed;
}
// Ignore the sector key as we don't support it
memcpy(out_data.data[i].data.data(), nfc_data.data() + sector_index,
sizeof(Common::Input::MifareData::data));
}
return Common::Input::NfcState::Success;
}
Common::Input::NfcState VirtualAmiibo::WriteMifareData(
const PadIdentifier& identifier_, const Common::Input::MifareRequest& request) {
if (state != State::TagNearby) {
return Common::Input::NfcState::WrongDeviceState;
}
if (status.tag_type != 1U << 6) {
return Common::Input::NfcState::InvalidTagType;
}
for (std::size_t i = 0; i < request.data.size(); i++) {
if (request.data[i].command == 0) {
continue;
}
const std::size_t sector_index =
request.data[i].sector * sizeof(Common::Input::MifareData::data);
if (nfc_data.size() < sector_index + sizeof(Common::Input::MifareData::data)) {
return Common::Input::NfcState::WriteFailed;
}
// Ignore the sector key as we don't support it
memcpy(nfc_data.data() + sector_index, request.data[i].data.data(),
sizeof(Common::Input::MifareData::data));
}
return Common::Input::NfcState::Success;
}
VirtualAmiibo::State VirtualAmiibo::GetCurrentState() const {
return state;
}
VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read,
Common::FS::FileType::BinaryFile};
std::vector<u8> data{};
if (!nfc_file.IsOpen()) {
return Info::UnableToLoad;
}
switch (nfc_file.GetSize()) {
case AmiiboSize:
case AmiiboSizeWithoutPassword:
case AmiiboSizeWithSignature:
data.resize(AmiiboSize);
if (nfc_file.Read(data) < AmiiboSizeWithoutPassword) {
return Info::NotAnAmiibo;
}
break;
case MifareSize:
data.resize(MifareSize);
if (nfc_file.Read(data) < MifareSize) {
return Info::NotAnAmiibo;
}
break;
default:
return Info::NotAnAmiibo;
}
file_path = filename;
return LoadAmiibo(data);
}
VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(std::span<u8> data) {
if (state != State::WaitingForAmiibo) {
return Info::WrongDeviceState;
}
switch (data.size_bytes()) {
case AmiiboSize:
case AmiiboSizeWithoutPassword:
case AmiiboSizeWithSignature:
nfc_data.resize(AmiiboSize);
status.tag_type = 1U << 1;
status.uuid_length = 7;
break;
case MifareSize:
nfc_data.resize(MifareSize);
status.tag_type = 1U << 6;
status.uuid_length = 4;
break;
default:
return Info::NotAnAmiibo;
}
status.uuid = {};
status.protocol = 1;
state = State::TagNearby;
status.state = Common::Input::NfcState::NewAmiibo,
memcpy(nfc_data.data(), data.data(), data.size_bytes());
memcpy(status.uuid.data(), nfc_data.data(), status.uuid_length);
SetNfc(identifier, status);
return Info::Success;
}
VirtualAmiibo::Info VirtualAmiibo::ReloadAmiibo() {
if (state == State::TagNearby) {
SetNfc(identifier, status);
return Info::Success;
}
return LoadAmiibo(file_path);
}
VirtualAmiibo::Info VirtualAmiibo::CloseAmiibo() {
if (state != State::TagNearby) {
return Info::Success;
}
state = State::WaitingForAmiibo;
status.state = Common::Input::NfcState::AmiiboRemoved;
SetNfc(identifier, status);
status.tag_type = 0;
return Info::Success;
}
std::string VirtualAmiibo::GetLastFilePath() const {
return file_path;
}
} // namespace InputCommon

View File

@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <span>
#include <string>
#include <vector>
#include "common/common_types.h"
#include "input_common/input_engine.h"
namespace Common::FS {
class IOFile;
}
namespace InputCommon {
class VirtualAmiibo final : public InputEngine {
public:
enum class State {
Disabled,
Initialized,
WaitingForAmiibo,
TagNearby,
};
enum class Info {
Success,
UnableToLoad,
NotAnAmiibo,
WrongDeviceState,
Unknown,
};
explicit VirtualAmiibo(std::string input_engine_);
~VirtualAmiibo() override;
// Sets polling mode to a controller
Common::Input::DriverResult SetPollingMode(
const PadIdentifier& identifier_, const Common::Input::PollingMode polling_mode_) override;
Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override;
Common::Input::NfcState StartNfcPolling(const PadIdentifier& identifier_) override;
Common::Input::NfcState StopNfcPolling(const PadIdentifier& identifier_) override;
Common::Input::NfcState ReadAmiiboData(const PadIdentifier& identifier_,
std::vector<u8>& out_data) override;
Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier_,
const std::vector<u8>& data) override;
Common::Input::NfcState ReadMifareData(const PadIdentifier& identifier_,
const Common::Input::MifareRequest& data,
Common::Input::MifareRequest& out_data) override;
Common::Input::NfcState WriteMifareData(const PadIdentifier& identifier_,
const Common::Input::MifareRequest& data) override;
State GetCurrentState() const;
Info LoadAmiibo(const std::string& amiibo_file);
Info LoadAmiibo(std::span<u8> data);
Info ReloadAmiibo();
Info CloseAmiibo();
std::string GetLastFilePath() const;
private:
static constexpr std::size_t AmiiboSize = 0x21C;
static constexpr std::size_t AmiiboSizeWithoutPassword = AmiiboSize - 0x8;
static constexpr std::size_t AmiiboSizeWithSignature = AmiiboSize + 0x20;
static constexpr std::size_t MifareSize = 0x400;
std::string file_path{};
State state{State::Disabled};
std::vector<u8> nfc_data;
Common::Input::NfcStatus status;
Common::Input::PollingMode polling_mode{Common::Input::PollingMode::Passive};
};
} // namespace InputCommon

View File

@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "input_common/drivers/virtual_gamepad.h"
namespace InputCommon {
constexpr std::size_t PlayerIndexCount = 10;
VirtualGamepad::VirtualGamepad(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
for (std::size_t i = 0; i < PlayerIndexCount; i++) {
PreSetController(GetIdentifier(i));
}
}
void VirtualGamepad::SetButtonState(std::size_t player_index, int button_id, bool value) {
if (player_index > PlayerIndexCount) {
return;
}
const auto identifier = GetIdentifier(player_index);
SetButton(identifier, button_id, value);
}
void VirtualGamepad::SetButtonState(std::size_t player_index, VirtualButton button_id, bool value) {
SetButtonState(player_index, static_cast<int>(button_id), value);
}
void VirtualGamepad::SetStickPosition(std::size_t player_index, int axis_id, float x_value,
float y_value) {
if (player_index > PlayerIndexCount) {
return;
}
const auto identifier = GetIdentifier(player_index);
SetAxis(identifier, axis_id * 2, x_value);
SetAxis(identifier, (axis_id * 2) + 1, y_value);
}
void VirtualGamepad::SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
float y_value) {
SetStickPosition(player_index, static_cast<int>(axis_id), x_value, y_value);
}
void VirtualGamepad::SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x,
float gyro_y, float gyro_z, float accel_x, float accel_y,
float accel_z) {
const auto identifier = GetIdentifier(player_index);
const BasicMotion motion_data{
.gyro_x = gyro_x,
.gyro_y = gyro_y,
.gyro_z = gyro_z,
.accel_x = accel_x,
.accel_y = accel_y,
.accel_z = accel_z,
.delta_timestamp = delta_timestamp,
};
SetMotion(identifier, 0, motion_data);
}
void VirtualGamepad::ResetControllers() {
for (std::size_t i = 0; i < PlayerIndexCount; i++) {
SetStickPosition(i, VirtualStick::Left, 0.0f, 0.0f);
SetStickPosition(i, VirtualStick::Right, 0.0f, 0.0f);
SetButtonState(i, VirtualButton::ButtonA, false);
SetButtonState(i, VirtualButton::ButtonB, false);
SetButtonState(i, VirtualButton::ButtonX, false);
SetButtonState(i, VirtualButton::ButtonY, false);
SetButtonState(i, VirtualButton::StickL, false);
SetButtonState(i, VirtualButton::StickR, false);
SetButtonState(i, VirtualButton::TriggerL, false);
SetButtonState(i, VirtualButton::TriggerR, false);
SetButtonState(i, VirtualButton::TriggerZL, false);
SetButtonState(i, VirtualButton::TriggerZR, false);
SetButtonState(i, VirtualButton::ButtonPlus, false);
SetButtonState(i, VirtualButton::ButtonMinus, false);
SetButtonState(i, VirtualButton::ButtonLeft, false);
SetButtonState(i, VirtualButton::ButtonUp, false);
SetButtonState(i, VirtualButton::ButtonRight, false);
SetButtonState(i, VirtualButton::ButtonDown, false);
SetButtonState(i, VirtualButton::ButtonSL, false);
SetButtonState(i, VirtualButton::ButtonSR, false);
SetButtonState(i, VirtualButton::ButtonHome, false);
SetButtonState(i, VirtualButton::ButtonCapture, false);
}
}
PadIdentifier VirtualGamepad::GetIdentifier(std::size_t player_index) const {
return {
.guid = Common::UUID{},
.port = player_index,
.pad = 0,
};
}
} // namespace InputCommon

View File

@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A virtual controller that is always assigned to the game input
*/
class VirtualGamepad final : public InputEngine {
public:
enum class VirtualButton {
ButtonA,
ButtonB,
ButtonX,
ButtonY,
StickL,
StickR,
TriggerL,
TriggerR,
TriggerZL,
TriggerZR,
ButtonPlus,
ButtonMinus,
ButtonLeft,
ButtonUp,
ButtonRight,
ButtonDown,
ButtonSL,
ButtonSR,
ButtonHome,
ButtonCapture,
};
enum class VirtualStick {
Left = 0,
Right = 1,
};
explicit VirtualGamepad(std::string input_engine_);
/**
* Sets the status of all buttons bound with the key to pressed
* @param player_index the player number that will take this action
* @param button_id the id of the button
* @param value indicates if the button is pressed or not
*/
void SetButtonState(std::size_t player_index, int button_id, bool value);
void SetButtonState(std::size_t player_index, VirtualButton button_id, bool value);
/**
* Sets the status of a stick to a specific player index
* @param player_index the player number that will take this action
* @param axis_id the id of the axis to move
* @param x_value the position of the stick in the x axis
* @param y_value the position of the stick in the y axis
*/
void SetStickPosition(std::size_t player_index, int axis_id, float x_value, float y_value);
void SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
float y_value);
/**
* Sets the status of the motion sensor to a specific player index
* @param player_index the player number that will take this action
* @param delta_timestamp time passed since last reading
* @param gyro_x,gyro_y,gyro_z the gyro sensor readings
* @param accel_x,accel_y,accel_z the accelerometer reading
*/
void SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
float gyro_z, float accel_x, float accel_y, float accel_z);
/// Restores all inputs into the neutral position
void ResetControllers();
private:
/// Returns the correct identifier corresponding to the player index
PadIdentifier GetIdentifier(std::size_t player_index) const;
};
} // namespace InputCommon

View File

@@ -0,0 +1,725 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/input.h"
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "common/swap.h"
#include "common/thread.h"
#include "input_common/helpers/joycon_driver.h"
#include "input_common/helpers/joycon_protocol/calibration.h"
#include "input_common/helpers/joycon_protocol/generic_functions.h"
#include "input_common/helpers/joycon_protocol/irs.h"
#include "input_common/helpers/joycon_protocol/nfc.h"
#include "input_common/helpers/joycon_protocol/poller.h"
#include "input_common/helpers/joycon_protocol/ringcon.h"
#include "input_common/helpers/joycon_protocol/rumble.h"
namespace InputCommon::Joycon {
JoyconDriver::JoyconDriver(std::size_t port_) : port{port_} {
hidapi_handle = std::make_shared<JoyconHandle>();
}
JoyconDriver::~JoyconDriver() {
Stop();
}
void JoyconDriver::Stop() {
is_connected = false;
input_thread = {};
}
Common::Input::DriverResult JoyconDriver::RequestDeviceAccess(SDL_hid_device_info* device_info) {
std::scoped_lock lock{mutex};
handle_device_type = ControllerType::None;
GetDeviceType(device_info, handle_device_type);
if (handle_device_type == ControllerType::None) {
return Common::Input::DriverResult::UnsupportedControllerType;
}
hidapi_handle->handle =
SDL_hid_open(device_info->vendor_id, device_info->product_id, device_info->serial_number);
std::memcpy(&handle_serial_number, device_info->serial_number, 15);
if (!hidapi_handle->handle) {
LOG_ERROR(Input, "Citron can't gain access to this device: ID {:04X}:{:04X}.",
device_info->vendor_id, device_info->product_id);
return Common::Input::DriverResult::HandleInUse;
}
SDL_hid_set_nonblocking(hidapi_handle->handle, 1);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult JoyconDriver::InitializeDevice() {
if (!hidapi_handle->handle) {
return Common::Input::DriverResult::InvalidHandle;
}
std::scoped_lock lock{mutex};
disable_input_thread = true;
// Reset Counters
error_counter = 0;
hidapi_handle->packet_counter = 0;
// Reset external device status
starlink_connected = false;
ring_connected = false;
amiibo_detected = false;
// Set HW default configuration
vibration_enabled = true;
motion_enabled = true;
hidbus_enabled = false;
nfc_enabled = false;
passive_enabled = false;
irs_enabled = false;
input_only_device = false;
gyro_sensitivity = Joycon::GyroSensitivity::DPS2000;
gyro_performance = Joycon::GyroPerformance::HZ833;
accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8;
accelerometer_performance = Joycon::AccelerometerPerformance::HZ100;
// Initialize HW Protocols
calibration_protocol = std::make_unique<CalibrationProtocol>(hidapi_handle);
generic_protocol = std::make_unique<GenericProtocol>(hidapi_handle);
irs_protocol = std::make_unique<IrsProtocol>(hidapi_handle);
nfc_protocol = std::make_unique<NfcProtocol>(hidapi_handle);
ring_protocol = std::make_unique<RingConProtocol>(hidapi_handle);
rumble_protocol = std::make_unique<RumbleProtocol>(hidapi_handle);
// Get fixed joycon info
if (generic_protocol->GetVersionNumber(version) != Common::Input::DriverResult::Success) {
// If this command fails the device doesn't accept configuration commands
input_only_device = true;
}
if (!input_only_device) {
generic_protocol->SetLowPowerMode(false);
generic_protocol->GetColor(color);
if (handle_device_type == ControllerType::Pro) {
// Some 3rd party controllers aren't pro controllers
generic_protocol->GetControllerType(device_type);
} else {
device_type = handle_device_type;
}
generic_protocol->GetSerialNumber(serial_number);
}
supported_features = GetSupportedFeatures();
// Get Calibration data
calibration_protocol->GetLeftJoyStickCalibration(left_stick_calibration);
calibration_protocol->GetRightJoyStickCalibration(right_stick_calibration);
calibration_protocol->GetImuCalibration(motion_calibration);
// Set led status
generic_protocol->SetLedBlinkPattern(static_cast<u8>(1 + port));
// Apply HW configuration
SetPollingMode();
// Initialize joycon poller
joycon_poller = std::make_unique<JoyconPoller>(device_type, left_stick_calibration,
right_stick_calibration, motion_calibration);
// Start polling for data
is_connected = true;
if (!input_thread_running) {
input_thread =
std::jthread([this](std::stop_token stop_token) { InputThread(stop_token); });
}
disable_input_thread = false;
return Common::Input::DriverResult::Success;
}
void JoyconDriver::InputThread(std::stop_token stop_token) {
LOG_INFO(Input, "Joycon Adapter input thread started");
Common::SetCurrentThreadName("JoyconInput");
input_thread_running = true;
// Max update rate is 5ms, ensure we are always able to read a bit faster
constexpr int ThreadDelay = 3;
std::vector<u8> buffer(MaxBufferSize);
while (!stop_token.stop_requested()) {
int status = 0;
if (!IsInputThreadValid()) {
input_thread.request_stop();
continue;
}
// By disabling the input thread we can ensure custom commands will succeed as no package is
// skipped
if (!disable_input_thread) {
status = SDL_hid_read_timeout(hidapi_handle->handle, buffer.data(), buffer.size(),
ThreadDelay);
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(ThreadDelay));
}
if (IsPayloadCorrect(status, buffer)) {
OnNewData(buffer);
}
if (!vibration_queue.Empty()) {
VibrationValue vibration_value;
vibration_queue.Pop(vibration_value);
last_vibration_result = rumble_protocol->SendVibration(vibration_value);
}
// We can't keep up with vibrations. Start skipping.
while (vibration_queue.Size() > 6) {
vibration_queue.Pop();
}
std::this_thread::yield();
}
is_connected = false;
input_thread_running = false;
LOG_INFO(Input, "Joycon Adapter input thread stopped");
}
void JoyconDriver::OnNewData(std::span<u8> buffer) {
const auto report_mode = static_cast<ReportMode>(buffer[0]);
// Packages can be a little bit inconsistent. Average the delta time to provide a smoother
// motion experience
switch (report_mode) {
case ReportMode::STANDARD_FULL_60HZ:
case ReportMode::NFC_IR_MODE_60HZ:
case ReportMode::SIMPLE_HID_MODE: {
const auto now = std::chrono::steady_clock::now();
const auto new_delta_time = static_cast<u64>(
std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count());
delta_time = ((delta_time * 8) + (new_delta_time * 2)) / 10;
last_update = now;
joycon_poller->UpdateColor(color);
break;
}
default:
break;
}
const MotionStatus motion_status{
.is_enabled = motion_enabled,
.delta_time = delta_time,
.gyro_sensitivity = gyro_sensitivity,
.accelerometer_sensitivity = accelerometer_sensitivity,
};
// TODO: Remove this when calibration is properly loaded and not calculated
if (ring_connected && report_mode == ReportMode::STANDARD_FULL_60HZ) {
InputReportActive data{};
memcpy(&data, buffer.data(), sizeof(InputReportActive));
calibration_protocol->GetRingCalibration(ring_calibration, data.ring_input);
}
const RingStatus ring_status{
.is_enabled = ring_connected,
.default_value = ring_calibration.default_value,
.max_value = ring_calibration.max_value,
.min_value = ring_calibration.min_value,
};
if (irs_protocol->IsEnabled()) {
irs_protocol->RequestImage(buffer);
joycon_poller->UpdateCamera(irs_protocol->GetImage(), irs_protocol->GetIrsFormat());
}
if (nfc_protocol->IsPolling()) {
if (amiibo_detected) {
if (!nfc_protocol->HasAmiibo()) {
joycon_poller->UpdateAmiibo({});
amiibo_detected = false;
return;
}
}
if (!amiibo_detected) {
Joycon::TagInfo tag_info;
const auto result = nfc_protocol->GetTagInfo(tag_info);
if (result == Common::Input::DriverResult::Success) {
joycon_poller->UpdateAmiibo(tag_info);
amiibo_detected = true;
}
}
}
switch (report_mode) {
case ReportMode::STANDARD_FULL_60HZ:
joycon_poller->ReadActiveMode(buffer, motion_status, ring_status);
break;
case ReportMode::NFC_IR_MODE_60HZ:
joycon_poller->ReadNfcIRMode(buffer, motion_status);
break;
case ReportMode::SIMPLE_HID_MODE:
joycon_poller->ReadPassiveMode(buffer);
break;
case ReportMode::SUBCMD_REPLY:
LOG_DEBUG(Input, "Unhandled command reply");
break;
default:
LOG_ERROR(Input, "Report mode not Implemented {}", report_mode);
break;
}
}
Common::Input::DriverResult JoyconDriver::SetPollingMode() {
SCOPE_EXIT {
disable_input_thread = false;
};
disable_input_thread = true;
rumble_protocol->EnableRumble(vibration_enabled && supported_features.vibration);
if (motion_enabled && supported_features.motion) {
generic_protocol->EnableImu(true);
generic_protocol->SetImuConfig(gyro_sensitivity, gyro_performance,
accelerometer_sensitivity, accelerometer_performance);
} else {
generic_protocol->EnableImu(false);
}
if (input_only_device) {
return Common::Input::DriverResult::NotSupported;
}
if (irs_protocol->IsEnabled()) {
irs_protocol->DisableIrs();
}
if (nfc_protocol->IsEnabled()) {
amiibo_detected = false;
nfc_protocol->DisableNfc();
}
if (ring_protocol->IsEnabled()) {
ring_connected = false;
ring_protocol->DisableRingCon();
}
if (irs_enabled && supported_features.irs) {
auto result = irs_protocol->EnableIrs();
if (result == Common::Input::DriverResult::Success) {
return result;
}
irs_protocol->DisableIrs();
LOG_ERROR(Input, "Error enabling IRS");
return result;
}
if (nfc_enabled && supported_features.nfc) {
auto result = nfc_protocol->EnableNfc();
if (result == Common::Input::DriverResult::Success) {
return result;
}
nfc_protocol->DisableNfc();
LOG_ERROR(Input, "Error enabling NFC");
return result;
}
if (hidbus_enabled && supported_features.hidbus) {
auto result = ring_protocol->EnableRingCon();
if (result == Common::Input::DriverResult::Success) {
result = ring_protocol->StartRingconPolling();
}
if (result == Common::Input::DriverResult::Success) {
ring_connected = true;
return result;
}
ring_connected = false;
ring_protocol->DisableRingCon();
LOG_ERROR(Input, "Error enabling Ringcon");
return result;
}
if (passive_enabled && supported_features.passive) {
const auto result = generic_protocol->EnablePassiveMode();
if (result == Common::Input::DriverResult::Success) {
return result;
}
LOG_ERROR(Input, "Error enabling passive mode");
}
// Default Mode
const auto result = generic_protocol->EnableActiveMode();
if (result != Common::Input::DriverResult::Success) {
LOG_ERROR(Input, "Error enabling active mode");
}
// Switch calls this function after enabling active mode
generic_protocol->TriggersElapsed();
return result;
}
JoyconDriver::SupportedFeatures JoyconDriver::GetSupportedFeatures() {
SupportedFeatures features{
.passive = true,
.motion = true,
.vibration = true,
};
if (input_only_device) {
return features;
}
if (device_type == ControllerType::Right) {
features.nfc = true;
features.irs = true;
features.hidbus = true;
}
if (device_type == ControllerType::Pro) {
features.nfc = true;
}
return features;
}
bool JoyconDriver::IsInputThreadValid() const {
if (!is_connected.load()) {
return false;
}
if (hidapi_handle->handle == nullptr) {
return false;
}
// Controller is not responding. Terminate connection
if (error_counter > MaxErrorCount) {
return false;
}
return true;
}
bool JoyconDriver::IsPayloadCorrect(int status, std::span<const u8> buffer) {
if (status <= -1) {
error_counter++;
return false;
}
// There's no new data
if (status == 0) {
return false;
}
// No reply ever starts with zero
if (buffer[0] == 0x00) {
error_counter++;
return false;
}
error_counter = 0;
return true;
}
Common::Input::DriverResult JoyconDriver::SetVibration(const VibrationValue& vibration) {
std::scoped_lock lock{mutex};
if (disable_input_thread) {
return Common::Input::DriverResult::HandleInUse;
}
vibration_queue.Push(vibration);
return last_vibration_result;
}
Common::Input::DriverResult JoyconDriver::SetLedConfig(u8 led_pattern) {
std::scoped_lock lock{mutex};
if (disable_input_thread) {
return Common::Input::DriverResult::HandleInUse;
}
return generic_protocol->SetLedPattern(led_pattern);
}
Common::Input::DriverResult JoyconDriver::SetIrsConfig(IrsMode mode_, IrsResolution format_) {
std::scoped_lock lock{mutex};
if (disable_input_thread) {
return Common::Input::DriverResult::HandleInUse;
}
disable_input_thread = true;
const auto result = irs_protocol->SetIrsConfig(mode_, format_);
disable_input_thread = false;
return result;
}
Common::Input::DriverResult JoyconDriver::SetPassiveMode() {
std::scoped_lock lock{mutex};
motion_enabled = false;
hidbus_enabled = false;
nfc_enabled = false;
passive_enabled = true;
irs_enabled = false;
return SetPollingMode();
}
Common::Input::DriverResult JoyconDriver::SetActiveMode() {
if (is_ring_disabled_by_irs) {
is_ring_disabled_by_irs = false;
SetActiveMode();
return SetRingConMode();
}
std::scoped_lock lock{mutex};
motion_enabled = true;
hidbus_enabled = false;
nfc_enabled = false;
passive_enabled = false;
irs_enabled = false;
return SetPollingMode();
}
Common::Input::DriverResult JoyconDriver::SetIrMode() {
std::scoped_lock lock{mutex};
if (!supported_features.irs) {
return Common::Input::DriverResult::NotSupported;
}
if (ring_connected) {
is_ring_disabled_by_irs = true;
}
motion_enabled = false;
hidbus_enabled = false;
nfc_enabled = false;
passive_enabled = false;
irs_enabled = true;
return SetPollingMode();
}
Common::Input::DriverResult JoyconDriver::SetNfcMode() {
std::scoped_lock lock{mutex};
if (!supported_features.nfc) {
return Common::Input::DriverResult::NotSupported;
}
motion_enabled = true;
hidbus_enabled = false;
nfc_enabled = true;
passive_enabled = false;
irs_enabled = false;
return SetPollingMode();
}
Common::Input::DriverResult JoyconDriver::SetRingConMode() {
std::scoped_lock lock{mutex};
if (!supported_features.hidbus) {
return Common::Input::DriverResult::NotSupported;
}
motion_enabled = true;
hidbus_enabled = true;
nfc_enabled = false;
passive_enabled = false;
irs_enabled = false;
const auto result = SetPollingMode();
if (!ring_connected) {
return Common::Input::DriverResult::NoDeviceDetected;
}
return result;
}
Common::Input::DriverResult JoyconDriver::StartNfcPolling() {
std::scoped_lock lock{mutex};
if (!supported_features.nfc) {
return Common::Input::DriverResult::NotSupported;
}
if (!nfc_protocol->IsEnabled()) {
return Common::Input::DriverResult::Disabled;
}
disable_input_thread = true;
const auto result = nfc_protocol->StartNFCPollingMode();
disable_input_thread = false;
return result;
}
Common::Input::DriverResult JoyconDriver::StopNfcPolling() {
std::scoped_lock lock{mutex};
if (!supported_features.nfc) {
return Common::Input::DriverResult::NotSupported;
}
if (!nfc_protocol->IsEnabled()) {
return Common::Input::DriverResult::Disabled;
}
disable_input_thread = true;
const auto result = nfc_protocol->StopNFCPollingMode();
disable_input_thread = false;
if (amiibo_detected) {
amiibo_detected = false;
joycon_poller->UpdateAmiibo({});
}
return result;
}
Common::Input::DriverResult JoyconDriver::ReadAmiiboData(std::vector<u8>& out_data) {
std::scoped_lock lock{mutex};
if (!supported_features.nfc) {
return Common::Input::DriverResult::NotSupported;
}
if (!nfc_protocol->IsEnabled()) {
return Common::Input::DriverResult::Disabled;
}
if (!amiibo_detected) {
return Common::Input::DriverResult::ErrorWritingData;
}
out_data.resize(0x21C);
disable_input_thread = true;
const auto result = nfc_protocol->ReadAmiibo(out_data);
disable_input_thread = false;
return result;
}
Common::Input::DriverResult JoyconDriver::WriteNfcData(std::span<const u8> data) {
std::scoped_lock lock{mutex};
if (!supported_features.nfc) {
return Common::Input::DriverResult::NotSupported;
}
if (!nfc_protocol->IsEnabled()) {
return Common::Input::DriverResult::Disabled;
}
if (!amiibo_detected) {
return Common::Input::DriverResult::ErrorWritingData;
}
disable_input_thread = true;
const auto result = nfc_protocol->WriteAmiibo(data);
disable_input_thread = false;
return result;
}
Common::Input::DriverResult JoyconDriver::ReadMifareData(std::span<const MifareReadChunk> data,
std::span<MifareReadData> out_data) {
std::scoped_lock lock{mutex};
if (!supported_features.nfc) {
return Common::Input::DriverResult::NotSupported;
}
if (!nfc_protocol->IsEnabled()) {
return Common::Input::DriverResult::Disabled;
}
if (!amiibo_detected) {
return Common::Input::DriverResult::ErrorWritingData;
}
disable_input_thread = true;
const auto result = nfc_protocol->ReadMifare(data, out_data);
disable_input_thread = false;
return result;
}
Common::Input::DriverResult JoyconDriver::WriteMifareData(std::span<const MifareWriteChunk> data) {
std::scoped_lock lock{mutex};
if (!supported_features.nfc) {
return Common::Input::DriverResult::NotSupported;
}
if (!nfc_protocol->IsEnabled()) {
return Common::Input::DriverResult::Disabled;
}
if (!amiibo_detected) {
return Common::Input::DriverResult::ErrorWritingData;
}
disable_input_thread = true;
const auto result = nfc_protocol->WriteMifare(data);
disable_input_thread = false;
return result;
}
bool JoyconDriver::IsConnected() const {
std::scoped_lock lock{mutex};
return is_connected.load();
}
bool JoyconDriver::IsVibrationEnabled() const {
std::scoped_lock lock{mutex};
return vibration_enabled;
}
FirmwareVersion JoyconDriver::GetDeviceVersion() const {
std::scoped_lock lock{mutex};
return version;
}
Color JoyconDriver::GetDeviceColor() const {
std::scoped_lock lock{mutex};
return color;
}
std::size_t JoyconDriver::GetDevicePort() const {
std::scoped_lock lock{mutex};
return port;
}
ControllerType JoyconDriver::GetDeviceType() const {
std::scoped_lock lock{mutex};
return device_type;
}
ControllerType JoyconDriver::GetHandleDeviceType() const {
std::scoped_lock lock{mutex};
return handle_device_type;
}
SerialNumber JoyconDriver::GetSerialNumber() const {
std::scoped_lock lock{mutex};
return serial_number;
}
SerialNumber JoyconDriver::GetHandleSerialNumber() const {
std::scoped_lock lock{mutex};
return handle_serial_number;
}
void JoyconDriver::SetCallbacks(const JoyconCallbacks& callbacks) {
joycon_poller->SetCallbacks(callbacks);
}
Common::Input::DriverResult JoyconDriver::GetDeviceType(SDL_hid_device_info* device_info,
ControllerType& controller_type) {
static constexpr std::array<std::pair<u32, ControllerType>, 6> supported_devices{
std::pair<u32, ControllerType>{0x2006, ControllerType::Left},
{0x2007, ControllerType::Right},
{0x2009, ControllerType::Pro},
};
constexpr u16 nintendo_vendor_id = 0x057e;
controller_type = ControllerType::None;
if (device_info->vendor_id != nintendo_vendor_id) {
return Common::Input::DriverResult::UnsupportedControllerType;
}
for (const auto& [product_id, type] : supported_devices) {
if (device_info->product_id == static_cast<u16>(product_id)) {
controller_type = type;
return Common::Input::DriverResult::Success;
}
}
return Common::Input::DriverResult::UnsupportedControllerType;
}
Common::Input::DriverResult JoyconDriver::GetSerialNumber(SDL_hid_device_info* device_info,
SerialNumber& serial_number) {
if (device_info->serial_number == nullptr) {
return Common::Input::DriverResult::Unknown;
}
std::memcpy(&serial_number, device_info->serial_number, 15);
return Common::Input::DriverResult::Success;
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <functional>
#include <mutex>
#include <span>
#include <thread>
#include "common/threadsafe_queue.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace Common::Input {
enum class DriverResult;
}
namespace InputCommon::Joycon {
class CalibrationProtocol;
class GenericProtocol;
class IrsProtocol;
class NfcProtocol;
class JoyconPoller;
class RingConProtocol;
class RumbleProtocol;
class JoyconDriver final {
public:
explicit JoyconDriver(std::size_t port_);
~JoyconDriver();
Common::Input::DriverResult RequestDeviceAccess(SDL_hid_device_info* device_info);
Common::Input::DriverResult InitializeDevice();
void Stop();
bool IsConnected() const;
bool IsVibrationEnabled() const;
FirmwareVersion GetDeviceVersion() const;
Color GetDeviceColor() const;
std::size_t GetDevicePort() const;
ControllerType GetDeviceType() const;
ControllerType GetHandleDeviceType() const;
SerialNumber GetSerialNumber() const;
SerialNumber GetHandleSerialNumber() const;
Common::Input::DriverResult SetVibration(const VibrationValue& vibration);
Common::Input::DriverResult SetLedConfig(u8 led_pattern);
Common::Input::DriverResult SetIrsConfig(IrsMode mode_, IrsResolution format_);
Common::Input::DriverResult SetPassiveMode();
Common::Input::DriverResult SetActiveMode();
Common::Input::DriverResult SetIrMode();
Common::Input::DriverResult SetNfcMode();
Common::Input::DriverResult SetRingConMode();
Common::Input::DriverResult StartNfcPolling();
Common::Input::DriverResult StopNfcPolling();
Common::Input::DriverResult ReadAmiiboData(std::vector<u8>& out_data);
Common::Input::DriverResult WriteNfcData(std::span<const u8> data);
Common::Input::DriverResult ReadMifareData(std::span<const MifareReadChunk> request,
std::span<MifareReadData> out_data);
Common::Input::DriverResult WriteMifareData(std::span<const MifareWriteChunk> request);
void SetCallbacks(const JoyconCallbacks& callbacks);
// Returns device type from hidapi handle
static Common::Input::DriverResult GetDeviceType(SDL_hid_device_info* device_info,
ControllerType& controller_type);
// Returns serial number from hidapi handle
static Common::Input::DriverResult GetSerialNumber(SDL_hid_device_info* device_info,
SerialNumber& serial_number);
private:
struct SupportedFeatures {
bool passive{};
bool hidbus{};
bool irs{};
bool motion{};
bool nfc{};
bool vibration{};
};
/// Main thread, actively request new data from the handle
void InputThread(std::stop_token stop_token);
/// Called every time a valid package arrives
void OnNewData(std::span<u8> buffer);
/// Updates device configuration to enable or disable features
Common::Input::DriverResult SetPollingMode();
/// Returns true if input thread is valid and doesn't need to be stopped
bool IsInputThreadValid() const;
/// Returns true if the data should be interpreted. Otherwise the error counter is incremented
bool IsPayloadCorrect(int status, std::span<const u8> buffer);
/// Returns a list of supported features that can be enabled on this device
SupportedFeatures GetSupportedFeatures();
// Protocol Features
std::unique_ptr<CalibrationProtocol> calibration_protocol;
std::unique_ptr<GenericProtocol> generic_protocol;
std::unique_ptr<IrsProtocol> irs_protocol;
std::unique_ptr<NfcProtocol> nfc_protocol;
std::unique_ptr<JoyconPoller> joycon_poller;
std::unique_ptr<RingConProtocol> ring_protocol;
std::unique_ptr<RumbleProtocol> rumble_protocol;
// Connection status
std::atomic<bool> is_connected{};
u64 delta_time;
std::size_t error_counter{};
std::shared_ptr<JoyconHandle> hidapi_handle;
std::chrono::time_point<std::chrono::steady_clock> last_update;
// External device status
bool starlink_connected{};
bool ring_connected{};
bool amiibo_detected{};
bool is_ring_disabled_by_irs{};
// Hardware configuration
u8 leds{};
ReportMode mode{};
bool input_only_device{};
bool passive_enabled{}; // Low power mode, Ideal for multiple controllers at the same time
bool hidbus_enabled{}; // External device support
bool irs_enabled{}; // Infrared camera input
bool motion_enabled{}; // Enables motion input
bool nfc_enabled{}; // Enables Amiibo detection
bool vibration_enabled{}; // Allows vibrations
// Calibration data
GyroSensitivity gyro_sensitivity{};
GyroPerformance gyro_performance{};
AccelerometerSensitivity accelerometer_sensitivity{};
AccelerometerPerformance accelerometer_performance{};
JoyStickCalibration left_stick_calibration{};
JoyStickCalibration right_stick_calibration{};
MotionCalibration motion_calibration{};
RingCalibration ring_calibration{};
// Fixed joycon info
FirmwareVersion version{};
Color color{};
std::size_t port{};
ControllerType device_type{}; // Device type reported by controller
ControllerType handle_device_type{}; // Device type reported by hidapi
SerialNumber serial_number{}; // Serial number reported by controller
SerialNumber handle_serial_number{}; // Serial number type reported by hidapi
SupportedFeatures supported_features{};
/// Queue of vibration request to controllers
Common::Input::DriverResult last_vibration_result{Common::Input::DriverResult::Success};
Common::SPSCQueue<VibrationValue> vibration_queue;
// Thread related
mutable std::mutex mutex;
std::jthread input_thread;
bool input_thread_running{};
bool disable_input_thread{};
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,221 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include "common/input.h"
#include "input_common/helpers/joycon_protocol/calibration.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
CalibrationProtocol::CalibrationProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
Common::Input::DriverResult CalibrationProtocol::GetLeftJoyStickCalibration(
JoyStickCalibration& calibration) {
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
JoystickLeftSpiCalibration spi_calibration{};
bool has_user_calibration = false;
calibration = {};
if (result == Common::Input::DriverResult::Success) {
result = HasUserCalibration(SpiAddress::USER_LEFT_MAGIC, has_user_calibration);
}
// Read User defined calibration
if (result == Common::Input::DriverResult::Success && has_user_calibration) {
result = ReadSPI(SpiAddress::USER_LEFT_DATA, spi_calibration);
}
// Read Factory calibration
if (result == Common::Input::DriverResult::Success && !has_user_calibration) {
result = ReadSPI(SpiAddress::FACT_LEFT_DATA, spi_calibration);
}
if (result == Common::Input::DriverResult::Success) {
calibration.x.center = GetXAxisCalibrationValue(spi_calibration.center);
calibration.y.center = GetYAxisCalibrationValue(spi_calibration.center);
calibration.x.min = GetXAxisCalibrationValue(spi_calibration.min);
calibration.y.min = GetYAxisCalibrationValue(spi_calibration.min);
calibration.x.max = GetXAxisCalibrationValue(spi_calibration.max);
calibration.y.max = GetYAxisCalibrationValue(spi_calibration.max);
}
// Set a valid default calibration if data is missing
ValidateCalibration(calibration);
return result;
}
Common::Input::DriverResult CalibrationProtocol::GetRightJoyStickCalibration(
JoyStickCalibration& calibration) {
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
JoystickRightSpiCalibration spi_calibration{};
bool has_user_calibration = false;
calibration = {};
if (result == Common::Input::DriverResult::Success) {
result = HasUserCalibration(SpiAddress::USER_RIGHT_MAGIC, has_user_calibration);
}
// Read User defined calibration
if (result == Common::Input::DriverResult::Success && has_user_calibration) {
result = ReadSPI(SpiAddress::USER_RIGHT_DATA, spi_calibration);
}
// Read Factory calibration
if (result == Common::Input::DriverResult::Success && !has_user_calibration) {
result = ReadSPI(SpiAddress::FACT_RIGHT_DATA, spi_calibration);
}
if (result == Common::Input::DriverResult::Success) {
calibration.x.center = GetXAxisCalibrationValue(spi_calibration.center);
calibration.y.center = GetYAxisCalibrationValue(spi_calibration.center);
calibration.x.min = GetXAxisCalibrationValue(spi_calibration.min);
calibration.y.min = GetYAxisCalibrationValue(spi_calibration.min);
calibration.x.max = GetXAxisCalibrationValue(spi_calibration.max);
calibration.y.max = GetYAxisCalibrationValue(spi_calibration.max);
}
// Set a valid default calibration if data is missing
ValidateCalibration(calibration);
return result;
}
Common::Input::DriverResult CalibrationProtocol::GetImuCalibration(MotionCalibration& calibration) {
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
ImuSpiCalibration spi_calibration{};
bool has_user_calibration = false;
calibration = {};
if (result == Common::Input::DriverResult::Success) {
result = HasUserCalibration(SpiAddress::USER_IMU_MAGIC, has_user_calibration);
}
// Read User defined calibration
if (result == Common::Input::DriverResult::Success && has_user_calibration) {
result = ReadSPI(SpiAddress::USER_IMU_DATA, spi_calibration);
}
// Read Factory calibration
if (result == Common::Input::DriverResult::Success && !has_user_calibration) {
result = ReadSPI(SpiAddress::FACT_IMU_DATA, spi_calibration);
}
if (result == Common::Input::DriverResult::Success) {
calibration.accelerometer[0].offset = spi_calibration.accelerometer_offset[0];
calibration.accelerometer[1].offset = spi_calibration.accelerometer_offset[1];
calibration.accelerometer[2].offset = spi_calibration.accelerometer_offset[2];
calibration.accelerometer[0].scale = spi_calibration.accelerometer_scale[0];
calibration.accelerometer[1].scale = spi_calibration.accelerometer_scale[1];
calibration.accelerometer[2].scale = spi_calibration.accelerometer_scale[2];
calibration.gyro[0].offset = spi_calibration.gyroscope_offset[0];
calibration.gyro[1].offset = spi_calibration.gyroscope_offset[1];
calibration.gyro[2].offset = spi_calibration.gyroscope_offset[2];
calibration.gyro[0].scale = spi_calibration.gyroscope_scale[0];
calibration.gyro[1].scale = spi_calibration.gyroscope_scale[1];
calibration.gyro[2].scale = spi_calibration.gyroscope_scale[2];
}
ValidateCalibration(calibration);
return result;
}
Common::Input::DriverResult CalibrationProtocol::GetRingCalibration(RingCalibration& calibration,
s16 current_value) {
constexpr s16 DefaultRingRange{800};
// TODO: Get default calibration form ring itself
if (ring_data_max == 0 && ring_data_min == 0) {
ring_data_max = current_value + DefaultRingRange;
ring_data_min = current_value - DefaultRingRange;
ring_data_default = current_value;
}
ring_data_max = std::max(ring_data_max, current_value);
ring_data_min = std::min(ring_data_min, current_value);
calibration = {
.default_value = ring_data_default,
.max_value = ring_data_max,
.min_value = ring_data_min,
};
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult CalibrationProtocol::HasUserCalibration(SpiAddress address,
bool& has_user_calibration) {
MagicSpiCalibration spi_magic{};
const Common::Input::DriverResult result{ReadSPI(address, spi_magic)};
has_user_calibration = false;
if (result == Common::Input::DriverResult::Success) {
has_user_calibration = spi_magic.first == CalibrationMagic::USR_MAGIC_0 &&
spi_magic.second == CalibrationMagic::USR_MAGIC_1;
}
return result;
}
u16 CalibrationProtocol::GetXAxisCalibrationValue(std::span<u8> block) const {
return static_cast<u16>(((block[1] & 0x0F) << 8) | block[0]);
}
u16 CalibrationProtocol::GetYAxisCalibrationValue(std::span<u8> block) const {
return static_cast<u16>((block[2] << 4) | (block[1] >> 4));
}
void CalibrationProtocol::ValidateCalibration(JoyStickCalibration& calibration) {
constexpr u16 DefaultStickCenter{0x800};
constexpr u16 DefaultStickRange{0x6cc};
calibration.x.center = ValidateValue(calibration.x.center, DefaultStickCenter);
calibration.x.max = ValidateValue(calibration.x.max, DefaultStickRange);
calibration.x.min = ValidateValue(calibration.x.min, DefaultStickRange);
calibration.y.center = ValidateValue(calibration.y.center, DefaultStickCenter);
calibration.y.max = ValidateValue(calibration.y.max, DefaultStickRange);
calibration.y.min = ValidateValue(calibration.y.min, DefaultStickRange);
}
void CalibrationProtocol::ValidateCalibration(MotionCalibration& calibration) {
constexpr s16 DefaultAccelerometerScale{0x4000};
constexpr s16 DefaultGyroScale{0x3be7};
constexpr s16 DefaultOffset{0};
for (auto& sensor : calibration.accelerometer) {
sensor.scale = ValidateValue(sensor.scale, DefaultAccelerometerScale);
sensor.offset = ValidateValue(sensor.offset, DefaultOffset);
}
for (auto& sensor : calibration.gyro) {
sensor.scale = ValidateValue(sensor.scale, DefaultGyroScale);
sensor.offset = ValidateValue(sensor.offset, DefaultOffset);
}
}
u16 CalibrationProtocol::ValidateValue(u16 value, u16 default_value) const {
if (value == 0) {
return default_value;
}
if (value == 0xFFF) {
return default_value;
}
return value;
}
s16 CalibrationProtocol::ValidateValue(s16 value, s16 default_value) const {
if (value == 0) {
return default_value;
}
if (value == 0xFFF) {
return default_value;
}
return value;
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
namespace Common::Input {
enum class DriverResult;
}
namespace InputCommon::Joycon {
struct JoyStickCalibration;
struct IMUCalibration;
struct JoyconHandle;
} // namespace InputCommon::Joycon
namespace InputCommon::Joycon {
/// Driver functions related to retrieving calibration data from the device
class CalibrationProtocol final : private JoyconCommonProtocol {
public:
explicit CalibrationProtocol(std::shared_ptr<JoyconHandle> handle);
/**
* Sends a request to obtain the left stick calibration from memory
* @param is_factory_calibration if true factory values will be returned
* @returns JoyStickCalibration of the left joystick
*/
Common::Input::DriverResult GetLeftJoyStickCalibration(JoyStickCalibration& calibration);
/**
* Sends a request to obtain the right stick calibration from memory
* @param is_factory_calibration if true factory values will be returned
* @returns JoyStickCalibration of the right joystick
*/
Common::Input::DriverResult GetRightJoyStickCalibration(JoyStickCalibration& calibration);
/**
* Sends a request to obtain the motion calibration from memory
* @returns ImuCalibration of the motion sensor
*/
Common::Input::DriverResult GetImuCalibration(MotionCalibration& calibration);
/**
* Calculates on run time the proper calibration of the ring controller
* @returns RingCalibration of the ring sensor
*/
Common::Input::DriverResult GetRingCalibration(RingCalibration& calibration, s16 current_value);
private:
/// Returns true if the specified address corresponds to the magic value of user calibration
Common::Input::DriverResult HasUserCalibration(SpiAddress address, bool& has_user_calibration);
/// Converts a raw calibration block to an u16 value containing the x axis value
u16 GetXAxisCalibrationValue(std::span<u8> block) const;
/// Converts a raw calibration block to an u16 value containing the y axis value
u16 GetYAxisCalibrationValue(std::span<u8> block) const;
/// Ensures that all joystick calibration values are set
void ValidateCalibration(JoyStickCalibration& calibration);
/// Ensures that all motion calibration values are set
void ValidateCalibration(MotionCalibration& calibration);
/// Returns the default value if the value is either zero or 0xFFF
u16 ValidateValue(u16 value, u16 default_value) const;
/// Returns the default value if the value is either zero or 0xFFF
s16 ValidateValue(s16 value, s16 default_value) const;
s16 ring_data_max = 0;
s16 ring_data_default = 0;
s16 ring_data_min = 0;
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,321 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/input.h"
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/common_protocol.h"
namespace InputCommon::Joycon {
JoyconCommonProtocol::JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_)
: hidapi_handle{std::move(hidapi_handle_)} {}
u8 JoyconCommonProtocol::GetCounter() {
hidapi_handle->packet_counter = (hidapi_handle->packet_counter + 1) & 0x0F;
return hidapi_handle->packet_counter;
}
void JoyconCommonProtocol::SetBlocking() {
SDL_hid_set_nonblocking(hidapi_handle->handle, 0);
}
void JoyconCommonProtocol::SetNonBlocking() {
SDL_hid_set_nonblocking(hidapi_handle->handle, 1);
}
Common::Input::DriverResult JoyconCommonProtocol::GetDeviceType(ControllerType& controller_type) {
const auto result = ReadSPI(SpiAddress::DEVICE_TYPE, controller_type);
if (result == Common::Input::DriverResult::Success) {
// Fallback to 3rd party pro controllers
if (controller_type == ControllerType::None) {
controller_type = ControllerType::Pro;
}
}
return result;
}
Common::Input::DriverResult JoyconCommonProtocol::CheckDeviceAccess(
SDL_hid_device_info* device_info) {
ControllerType controller_type{ControllerType::None};
const auto result = GetDeviceType(controller_type);
if (result != Common::Input::DriverResult::Success || controller_type == ControllerType::None) {
return Common::Input::DriverResult::UnsupportedControllerType;
}
hidapi_handle->handle =
SDL_hid_open(device_info->vendor_id, device_info->product_id, device_info->serial_number);
if (!hidapi_handle->handle) {
LOG_ERROR(Input, "Citron can't gain access to this device: ID {:04X}:{:04X}.",
device_info->vendor_id, device_info->product_id);
return Common::Input::DriverResult::HandleInUse;
}
SetNonBlocking();
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult JoyconCommonProtocol::SetReportMode(ReportMode report_mode) {
const std::array<u8, 1> buffer{static_cast<u8>(report_mode)};
return SendSubCommand(SubCommand::SET_REPORT_MODE, buffer);
}
Common::Input::DriverResult JoyconCommonProtocol::SendRawData(std::span<const u8> buffer) {
const auto result = SDL_hid_write(hidapi_handle->handle, buffer.data(), buffer.size());
if (result == -1) {
return Common::Input::DriverResult::ErrorWritingData;
}
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult JoyconCommonProtocol::GetSubCommandResponse(
SubCommand sc, SubCommandResponse& output) {
constexpr int timeout_mili = 66;
constexpr int MaxTries = 10;
int tries = 0;
do {
int result = SDL_hid_read_timeout(hidapi_handle->handle, reinterpret_cast<u8*>(&output),
sizeof(SubCommandResponse), timeout_mili);
if (result < 1) {
LOG_ERROR(Input, "No response from joycon");
}
if (tries++ > MaxTries) {
return Common::Input::DriverResult::Timeout;
}
} while (output.input_report.report_mode != ReportMode::SUBCMD_REPLY &&
output.sub_command != sc);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc,
std::span<const u8> buffer,
SubCommandResponse& output) {
SubCommandPacket packet{
.output_report = OutputReport::RUMBLE_AND_SUBCMD,
.packet_counter = GetCounter(),
.sub_command = sc,
.command_data = {},
};
if (buffer.size() > packet.command_data.size()) {
return Common::Input::DriverResult::InvalidParameters;
}
memcpy(packet.command_data.data(), buffer.data(), buffer.size());
auto result = SendData(packet);
if (result != Common::Input::DriverResult::Success) {
return result;
}
return GetSubCommandResponse(sc, output);
}
Common::Input::DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc,
std::span<const u8> buffer) {
SubCommandResponse output{};
return SendSubCommand(sc, buffer, output);
}
Common::Input::DriverResult JoyconCommonProtocol::SendMCUCommand(SubCommand sc,
std::span<const u8> buffer) {
SubCommandPacket packet{
.output_report = OutputReport::MCU_DATA,
.packet_counter = GetCounter(),
.sub_command = sc,
.command_data = {},
};
if (buffer.size() > packet.command_data.size()) {
return Common::Input::DriverResult::InvalidParameters;
}
memcpy(packet.command_data.data(), buffer.data(), buffer.size());
return SendData(packet);
}
Common::Input::DriverResult JoyconCommonProtocol::SendVibrationReport(std::span<const u8> buffer) {
VibrationPacket packet{
.output_report = OutputReport::RUMBLE_ONLY,
.packet_counter = GetCounter(),
.vibration_data = {},
};
if (buffer.size() > packet.vibration_data.size()) {
return Common::Input::DriverResult::InvalidParameters;
}
memcpy(packet.vibration_data.data(), buffer.data(), buffer.size());
return SendData(packet);
}
Common::Input::DriverResult JoyconCommonProtocol::ReadRawSPI(SpiAddress addr,
std::span<u8> output) {
constexpr std::size_t HeaderSize = 5;
constexpr std::size_t MaxTries = 5;
std::size_t tries = 0;
SubCommandResponse response{};
std::array<u8, sizeof(ReadSpiPacket)> buffer{};
const ReadSpiPacket packet_data{
.spi_address = addr,
.size = static_cast<u8>(output.size()),
};
memcpy(buffer.data(), &packet_data, sizeof(ReadSpiPacket));
do {
const auto result = SendSubCommand(SubCommand::SPI_FLASH_READ, buffer, response);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (tries++ > MaxTries) {
return Common::Input::DriverResult::Timeout;
}
} while (response.spi_address != addr);
if (response.command_data.size() < packet_data.size + HeaderSize) {
return Common::Input::DriverResult::WrongReply;
}
// Remove header from output
memcpy(output.data(), response.command_data.data() + HeaderSize, packet_data.size);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult JoyconCommonProtocol::EnableMCU(bool enable) {
const std::array<u8, 1> mcu_state{static_cast<u8>(enable ? 1 : 0)};
const auto result = SendSubCommand(SubCommand::SET_MCU_STATE, mcu_state);
if (result != Common::Input::DriverResult::Success) {
LOG_ERROR(Input, "Failed with error {}", result);
}
return result;
}
Common::Input::DriverResult JoyconCommonProtocol::ConfigureMCU(const MCUConfig& config) {
LOG_DEBUG(Input, "ConfigureMCU");
std::array<u8, sizeof(MCUConfig)> config_buffer;
memcpy(config_buffer.data(), &config, sizeof(MCUConfig));
config_buffer[37] = CalculateMCU_CRC8(config_buffer.data() + 1, 36);
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, config_buffer);
if (result != Common::Input::DriverResult::Success) {
LOG_ERROR(Input, "Failed with error {}", result);
}
return result;
}
Common::Input::DriverResult JoyconCommonProtocol::GetMCUDataResponse(ReportMode report_mode,
MCUCommandResponse& output) {
constexpr int TimeoutMili = 200;
constexpr int MaxTries = 9;
int tries = 0;
do {
int result = SDL_hid_read_timeout(hidapi_handle->handle, reinterpret_cast<u8*>(&output),
sizeof(MCUCommandResponse), TimeoutMili);
if (result < 1) {
LOG_ERROR(Input, "No response from joycon attempt {}", tries);
}
if (tries++ > MaxTries) {
return Common::Input::DriverResult::Timeout;
}
} while (output.input_report.report_mode != report_mode ||
output.mcu_report == MCUReport::EmptyAwaitingCmd);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult JoyconCommonProtocol::SendMCUData(ReportMode report_mode,
MCUSubCommand sc,
std::span<const u8> buffer,
MCUCommandResponse& output) {
SubCommandPacket packet{
.output_report = OutputReport::MCU_DATA,
.packet_counter = GetCounter(),
.mcu_sub_command = sc,
.command_data = {},
};
if (buffer.size() > packet.command_data.size()) {
return Common::Input::DriverResult::InvalidParameters;
}
memcpy(packet.command_data.data(), buffer.data(), buffer.size());
auto result = SendData(packet);
if (result != Common::Input::DriverResult::Success) {
return result;
}
result = GetMCUDataResponse(report_mode, output);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult JoyconCommonProtocol::WaitSetMCUMode(ReportMode report_mode,
MCUMode mode) {
MCUCommandResponse output{};
constexpr std::size_t MaxTries{16};
std::size_t tries{};
do {
const auto result = SendMCUData(report_mode, MCUSubCommand::SetDeviceMode, {}, output);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (tries++ > MaxTries) {
return Common::Input::DriverResult::WrongReply;
}
} while (output.mcu_report != MCUReport::StateReport ||
output.mcu_data[6] != static_cast<u8>(mode));
return Common::Input::DriverResult::Success;
}
// crc-8-ccitt / polynomial 0x07 look up table
constexpr std::array<u8, 256> mcu_crc8_table = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3};
u8 JoyconCommonProtocol::CalculateMCU_CRC8(u8* buffer, u8 size) const {
u8 crc8 = 0x0;
for (int i = 0; i < size; ++i) {
crc8 = mcu_crc8_table[static_cast<u8>(crc8 ^ buffer[i])];
}
return crc8;
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,203 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <memory>
#include <span>
#include <vector>
#include "common/common_types.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
/// Joycon driver functions that handle low level communication
class JoyconCommonProtocol {
public:
explicit JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_);
/**
* Sets handle to blocking. In blocking mode, SDL_hid_read() will wait (block) until there is
* data to read before returning.
*/
void SetBlocking();
/**
* Sets handle to non blocking. In non-blocking mode calls to SDL_hid_read() will return
* immediately with a value of 0 if there is no data to be read
*/
void SetNonBlocking();
/**
* Sends a request to obtain the joycon type from device
* @returns controller type of the joycon
*/
Common::Input::DriverResult GetDeviceType(ControllerType& controller_type);
/**
* Verifies and sets the joycon_handle if device is valid
* @param device info from the driver
* @returns success if the device is valid
*/
Common::Input::DriverResult CheckDeviceAccess(SDL_hid_device_info* device);
/**
* Sends a request to set the polling mode of the joycon
* @param report_mode polling mode to be set
*/
Common::Input::DriverResult SetReportMode(Joycon::ReportMode report_mode);
/**
* Sends data to the joycon device
* @param buffer data to be send
*/
Common::Input::DriverResult SendRawData(std::span<const u8> buffer);
template <typename Output>
requires std::is_trivially_copyable_v<Output>
Common::Input::DriverResult SendData(const Output& output) {
std::array<u8, sizeof(Output)> buffer;
std::memcpy(buffer.data(), &output, sizeof(Output));
return SendRawData(buffer);
}
/**
* Waits for incoming data of the joycon device that matches the subcommand
* @param sub_command type of data to be returned
* @returns a buffer containing the response
*/
Common::Input::DriverResult GetSubCommandResponse(SubCommand sub_command,
SubCommandResponse& output);
/**
* Sends a sub command to the device and waits for it's reply
* @param sc sub command to be send
* @param buffer data to be send
* @returns output buffer containing the response
*/
Common::Input::DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer,
SubCommandResponse& output);
/**
* Sends a sub command to the device and waits for it's reply and ignores the output
* @param sc sub command to be send
* @param buffer data to be send
*/
Common::Input::DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer);
/**
* Sends a mcu command to the device
* @param sc sub command to be send
* @param buffer data to be send
*/
Common::Input::DriverResult SendMCUCommand(SubCommand sc, std::span<const u8> buffer);
/**
* Sends vibration data to the joycon
* @param buffer data to be send
*/
Common::Input::DriverResult SendVibrationReport(std::span<const u8> buffer);
/**
* Reads the SPI memory stored on the joycon
* @param Initial address location
* @returns output buffer containing the response
*/
Common::Input::DriverResult ReadRawSPI(SpiAddress addr, std::span<u8> output);
/**
* Reads the SPI memory stored on the joycon
* @param Initial address location
* @returns output object containing the response
*/
template <typename Output>
requires std::is_trivially_copyable_v<Output>
Common::Input::DriverResult ReadSPI(SpiAddress addr, Output& output) {
std::array<u8, sizeof(Output)> buffer;
output = {};
const auto result = ReadRawSPI(addr, buffer);
if (result != Common::Input::DriverResult::Success) {
return result;
}
std::memcpy(&output, buffer.data(), sizeof(Output));
return Common::Input::DriverResult::Success;
}
/**
* Enables MCU chip on the joycon
* @param enable if true the chip will be enabled
*/
Common::Input::DriverResult EnableMCU(bool enable);
/**
* Configures the MCU to the corresponding mode
* @param MCUConfig configuration
*/
Common::Input::DriverResult ConfigureMCU(const MCUConfig& config);
/**
* Waits until there's MCU data available. On timeout returns error
* @param report mode of the expected reply
* @returns a buffer containing the response
*/
Common::Input::DriverResult GetMCUDataResponse(ReportMode report_mode_,
MCUCommandResponse& output);
/**
* Sends data to the MCU chip and waits for it's reply
* @param report mode of the expected reply
* @param sub command to be send
* @param buffer data to be send
* @returns output buffer containing the response
*/
Common::Input::DriverResult SendMCUData(ReportMode report_mode, MCUSubCommand sc,
std::span<const u8> buffer, MCUCommandResponse& output);
/**
* Wait's until the MCU chip is on the specified mode
* @param report mode of the expected reply
* @param MCUMode configuration
*/
Common::Input::DriverResult WaitSetMCUMode(ReportMode report_mode, MCUMode mode);
/**
* Calculates the checksum from the MCU data
* @param buffer containing the data to be send
* @param size of the buffer in bytes
* @returns byte with the correct checksum
*/
u8 CalculateMCU_CRC8(u8* buffer, u8 size) const;
private:
/**
* Increments and returns the packet counter of the handle
* @param joycon_handle device to send the data
* @returns packet counter value
*/
u8 GetCounter();
std::shared_ptr<JoyconHandle> hidapi_handle;
};
class ScopedSetBlocking {
public:
explicit ScopedSetBlocking(JoyconCommonProtocol* self) : m_self{self} {
m_self->SetBlocking();
}
~ScopedSetBlocking() {
m_self->SetNonBlocking();
}
private:
JoyconCommonProtocol* m_self{};
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/input.h"
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/generic_functions.h"
namespace InputCommon::Joycon {
GenericProtocol::GenericProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
Common::Input::DriverResult GenericProtocol::EnablePassiveMode() {
ScopedSetBlocking sb(this);
return SetReportMode(ReportMode::SIMPLE_HID_MODE);
}
Common::Input::DriverResult GenericProtocol::EnableActiveMode() {
ScopedSetBlocking sb(this);
return SetReportMode(ReportMode::STANDARD_FULL_60HZ);
}
Common::Input::DriverResult GenericProtocol::SetLowPowerMode(bool enable) {
ScopedSetBlocking sb(this);
const std::array<u8, 1> buffer{static_cast<u8>(enable ? 1 : 0)};
return SendSubCommand(SubCommand::LOW_POWER_MODE, buffer);
}
Common::Input::DriverResult GenericProtocol::TriggersElapsed() {
ScopedSetBlocking sb(this);
return SendSubCommand(SubCommand::TRIGGERS_ELAPSED, {});
}
Common::Input::DriverResult GenericProtocol::GetDeviceInfo(DeviceInfo& device_info) {
ScopedSetBlocking sb(this);
SubCommandResponse output{};
const auto result = SendSubCommand(SubCommand::REQ_DEV_INFO, {}, output);
device_info = {};
if (result == Common::Input::DriverResult::Success) {
device_info = output.device_info;
}
return result;
}
Common::Input::DriverResult GenericProtocol::GetControllerType(ControllerType& controller_type) {
return GetDeviceType(controller_type);
}
Common::Input::DriverResult GenericProtocol::EnableImu(bool enable) {
ScopedSetBlocking sb(this);
const std::array<u8, 1> buffer{static_cast<u8>(enable ? 1 : 0)};
return SendSubCommand(SubCommand::ENABLE_IMU, buffer);
}
Common::Input::DriverResult GenericProtocol::SetImuConfig(GyroSensitivity gsen,
GyroPerformance gfrec,
AccelerometerSensitivity asen,
AccelerometerPerformance afrec) {
ScopedSetBlocking sb(this);
const std::array<u8, 4> buffer{static_cast<u8>(gsen), static_cast<u8>(asen),
static_cast<u8>(gfrec), static_cast<u8>(afrec)};
return SendSubCommand(SubCommand::SET_IMU_SENSITIVITY, buffer);
}
Common::Input::DriverResult GenericProtocol::GetBattery(u32& battery_level) {
// This function is meant to request the high resolution battery status
battery_level = 0;
return Common::Input::DriverResult::NotSupported;
}
Common::Input::DriverResult GenericProtocol::GetColor(Color& color) {
ScopedSetBlocking sb(this);
std::array<u8, 12> buffer{};
const auto result = ReadRawSPI(SpiAddress::COLOR_DATA, buffer);
color = {};
if (result == Common::Input::DriverResult::Success) {
color.body = static_cast<u32>((buffer[0] << 16) | (buffer[1] << 8) | buffer[2]);
color.buttons = static_cast<u32>((buffer[3] << 16) | (buffer[4] << 8) | buffer[5]);
color.left_grip = static_cast<u32>((buffer[6] << 16) | (buffer[7] << 8) | buffer[8]);
color.right_grip = static_cast<u32>((buffer[9] << 16) | (buffer[10] << 8) | buffer[11]);
}
return result;
}
Common::Input::DriverResult GenericProtocol::GetSerialNumber(SerialNumber& serial_number) {
ScopedSetBlocking sb(this);
std::array<u8, 16> buffer{};
const auto result = ReadRawSPI(SpiAddress::SERIAL_NUMBER, buffer);
serial_number = {};
if (result == Common::Input::DriverResult::Success) {
memcpy(serial_number.data(), buffer.data() + 1, sizeof(SerialNumber));
}
return result;
}
Common::Input::DriverResult GenericProtocol::GetTemperature(u32& temperature) {
// Not all devices have temperature sensor
temperature = 25;
return Common::Input::DriverResult::NotSupported;
}
Common::Input::DriverResult GenericProtocol::GetVersionNumber(FirmwareVersion& version) {
DeviceInfo device_info{};
const auto result = GetDeviceInfo(device_info);
version = device_info.firmware;
return result;
}
Common::Input::DriverResult GenericProtocol::SetHomeLight() {
ScopedSetBlocking sb(this);
static constexpr std::array<u8, 3> buffer{0x0f, 0xf0, 0x00};
return SendSubCommand(SubCommand::SET_HOME_LIGHT, buffer);
}
Common::Input::DriverResult GenericProtocol::SetLedBusy() {
return Common::Input::DriverResult::NotSupported;
}
Common::Input::DriverResult GenericProtocol::SetLedPattern(u8 leds) {
ScopedSetBlocking sb(this);
const std::array<u8, 1> buffer{leds};
return SendSubCommand(SubCommand::SET_PLAYER_LIGHTS, buffer);
}
Common::Input::DriverResult GenericProtocol::SetLedBlinkPattern(u8 leds) {
return SetLedPattern(static_cast<u8>(leds << 4));
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace Common::Input {
enum class DriverResult;
}
namespace InputCommon::Joycon {
/// Joycon driver functions that easily implemented
class GenericProtocol final : private JoyconCommonProtocol {
public:
explicit GenericProtocol(std::shared_ptr<JoyconHandle> handle);
/// Enables passive mode. This mode only sends button data on change. Sticks will return digital
/// data instead of analog. Motion will be disabled
Common::Input::DriverResult EnablePassiveMode();
/// Enables active mode. This mode will return the current status every 5-15ms
Common::Input::DriverResult EnableActiveMode();
/// Enables or disables the low power mode
Common::Input::DriverResult SetLowPowerMode(bool enable);
/// Unknown function used by the switch
Common::Input::DriverResult TriggersElapsed();
/**
* Sends a request to obtain the joycon firmware and mac from handle
* @returns controller device info
*/
Common::Input::DriverResult GetDeviceInfo(DeviceInfo& controller_type);
/**
* Sends a request to obtain the joycon type from handle
* @returns controller type of the joycon
*/
Common::Input::DriverResult GetControllerType(ControllerType& controller_type);
/**
* Enables motion input
* @param enable if true motion data will be enabled
*/
Common::Input::DriverResult EnableImu(bool enable);
/**
* Configures the motion sensor with the specified parameters
* @param gsen gyroscope sensor sensitivity in degrees per second
* @param gfrec gyroscope sensor frequency in hertz
* @param asen accelerometer sensitivity in G force
* @param afrec accelerometer frequency in hertz
*/
Common::Input::DriverResult SetImuConfig(GyroSensitivity gsen, GyroPerformance gfrec,
AccelerometerSensitivity asen,
AccelerometerPerformance afrec);
/**
* Request battery level from the device
* @returns battery level
*/
Common::Input::DriverResult GetBattery(u32& battery_level);
/**
* Request joycon colors from the device
* @returns colors of the body and buttons
*/
Common::Input::DriverResult GetColor(Color& color);
/**
* Request joycon serial number from the device
* @returns 16 byte serial number
*/
Common::Input::DriverResult GetSerialNumber(SerialNumber& serial_number);
/**
* Request joycon serial number from the device
* @returns 16 byte serial number
*/
Common::Input::DriverResult GetTemperature(u32& temperature);
/**
* Request joycon serial number from the device
* @returns 16 byte serial number
*/
Common::Input::DriverResult GetVersionNumber(FirmwareVersion& version);
/**
* Sets home led behaviour
*/
Common::Input::DriverResult SetHomeLight();
/**
* Sets home led into a slow breathing state
*/
Common::Input::DriverResult SetLedBusy();
/**
* Sets the 4 player leds on the joycon on a solid state
* @params bit flag containing the led state
*/
Common::Input::DriverResult SetLedPattern(u8 leds);
/**
* Sets the 4 player leds on the joycon on a blinking state
* @returns bit flag containing the led state
*/
Common::Input::DriverResult SetLedBlinkPattern(u8 leds);
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,299 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/input.h"
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/irs.h"
namespace InputCommon::Joycon {
IrsProtocol::IrsProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
Common::Input::DriverResult IrsProtocol::EnableIrs() {
LOG_INFO(Input, "Enable IRS");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ);
}
if (result == Common::Input::DriverResult::Success) {
result = EnableMCU(true);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby);
}
if (result == Common::Input::DriverResult::Success) {
const MCUConfig config{
.command = MCUCommand::ConfigureMCU,
.sub_command = MCUSubCommand::SetMCUMode,
.mode = MCUMode::IR,
.crc = {},
};
result = ConfigureMCU(config);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::IR);
}
if (result == Common::Input::DriverResult::Success) {
result = ConfigureIrs();
}
if (result == Common::Input::DriverResult::Success) {
result = WriteRegistersStep1();
}
if (result == Common::Input::DriverResult::Success) {
result = WriteRegistersStep2();
}
is_enabled = true;
return result;
}
Common::Input::DriverResult IrsProtocol::DisableIrs() {
LOG_DEBUG(Input, "Disable IRS");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
result = EnableMCU(false);
}
is_enabled = false;
return result;
}
Common::Input::DriverResult IrsProtocol::SetIrsConfig(IrsMode mode, IrsResolution format) {
irs_mode = mode;
switch (format) {
case IrsResolution::Size320x240:
resolution_code = IrsResolutionCode::Size320x240;
fragments = IrsFragments::Size320x240;
resolution = IrsResolution::Size320x240;
break;
case IrsResolution::Size160x120:
resolution_code = IrsResolutionCode::Size160x120;
fragments = IrsFragments::Size160x120;
resolution = IrsResolution::Size160x120;
break;
case IrsResolution::Size80x60:
resolution_code = IrsResolutionCode::Size80x60;
fragments = IrsFragments::Size80x60;
resolution = IrsResolution::Size80x60;
break;
case IrsResolution::Size20x15:
resolution_code = IrsResolutionCode::Size20x15;
fragments = IrsFragments::Size20x15;
resolution = IrsResolution::Size20x15;
break;
case IrsResolution::Size40x30:
default:
resolution_code = IrsResolutionCode::Size40x30;
fragments = IrsFragments::Size40x30;
resolution = IrsResolution::Size40x30;
break;
}
// Restart feature
if (is_enabled) {
DisableIrs();
return EnableIrs();
}
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult IrsProtocol::RequestImage(std::span<u8> buffer) {
const u8 next_packet_fragment =
static_cast<u8>((packet_fragment + 1) % (static_cast<u8>(fragments) + 1));
if (buffer[0] == 0x31 && buffer[49] == 0x03) {
u8 new_packet_fragment = buffer[52];
if (new_packet_fragment == next_packet_fragment) {
packet_fragment = next_packet_fragment;
memcpy(buf_image.data() + (300 * packet_fragment), buffer.data() + 59, 300);
return RequestFrame(packet_fragment);
}
if (new_packet_fragment == packet_fragment) {
return RequestFrame(packet_fragment);
}
return ResendFrame(next_packet_fragment);
}
return RequestFrame(packet_fragment);
}
Common::Input::DriverResult IrsProtocol::ConfigureIrs() {
LOG_DEBUG(Input, "Configure IRS");
constexpr std::size_t max_tries = 28;
SubCommandResponse output{};
std::size_t tries = 0;
const IrsConfigure irs_configuration{
.command = MCUCommand::ConfigureIR,
.sub_command = MCUSubCommand::SetDeviceMode,
.irs_mode = IrsMode::ImageTransfer,
.number_of_fragments = fragments,
.mcu_major_version = 0x0500,
.mcu_minor_version = 0x1800,
.crc = {},
};
buf_image.resize((static_cast<u8>(fragments) + 1) * 300);
std::array<u8, sizeof(IrsConfigure)> request_data{};
memcpy(request_data.data(), &irs_configuration, sizeof(IrsConfigure));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
do {
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return Common::Input::DriverResult::WrongReply;
}
} while (output.command_data[0] != 0x0b);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult IrsProtocol::WriteRegistersStep1() {
LOG_DEBUG(Input, "WriteRegistersStep1");
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
constexpr std::size_t max_tries = 28;
SubCommandResponse output{};
std::size_t tries = 0;
const IrsWriteRegisters irs_registers{
.command = MCUCommand::ConfigureIR,
.sub_command = MCUSubCommand::WriteDeviceRegisters,
.number_of_registers = 0x9,
.registers =
{
IrsRegister{IrRegistersAddress::Resolution, static_cast<u8>(resolution_code)},
{IrRegistersAddress::ExposureLSB, static_cast<u8>(exposure & 0xff)},
{IrRegistersAddress::ExposureMSB, static_cast<u8>(exposure >> 8)},
{IrRegistersAddress::ExposureTime, 0x00},
{IrRegistersAddress::Leds, static_cast<u8>(leds)},
{IrRegistersAddress::DigitalGainLSB, static_cast<u8>((digital_gain & 0x0f) << 4)},
{IrRegistersAddress::DigitalGainMSB, static_cast<u8>((digital_gain & 0xf0) >> 4)},
{IrRegistersAddress::LedFilter, static_cast<u8>(led_filter)},
{IrRegistersAddress::WhitePixelThreshold, 0xc8},
},
.crc = {},
};
std::array<u8, sizeof(IrsWriteRegisters)> request_data{};
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
std::array<u8, 38> mcu_request{0x02};
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
mcu_request[37] = 0xFF;
if (result != Common::Input::DriverResult::Success) {
return result;
}
do {
result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
// First time we need to set the report mode
if (result == Common::Input::DriverResult::Success && tries == 0) {
result = SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
}
if (result == Common::Input::DriverResult::Success && tries == 0) {
GetSubCommandResponse(SubCommand::SET_MCU_CONFIG, output);
}
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return Common::Input::DriverResult::WrongReply;
}
} while (!(output.command_data[0] == 0x13 && output.command_data[2] == 0x07) &&
output.command_data[0] != 0x23);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult IrsProtocol::WriteRegistersStep2() {
LOG_DEBUG(Input, "WriteRegistersStep2");
constexpr std::size_t max_tries = 28;
SubCommandResponse output{};
std::size_t tries = 0;
const IrsWriteRegisters irs_registers{
.command = MCUCommand::ConfigureIR,
.sub_command = MCUSubCommand::WriteDeviceRegisters,
.number_of_registers = 0x8,
.registers =
{
IrsRegister{IrRegistersAddress::LedIntensityMSB,
static_cast<u8>(led_intensity >> 8)},
{IrRegistersAddress::LedIntensityLSB, static_cast<u8>(led_intensity & 0xff)},
{IrRegistersAddress::ImageFlip, static_cast<u8>(image_flip)},
{IrRegistersAddress::DenoiseSmoothing, static_cast<u8>((denoise >> 16) & 0xff)},
{IrRegistersAddress::DenoiseEdge, static_cast<u8>((denoise >> 8) & 0xff)},
{IrRegistersAddress::DenoiseColor, static_cast<u8>(denoise & 0xff)},
{IrRegistersAddress::UpdateTime, 0x2d},
{IrRegistersAddress::FinalizeConfig, 0x01},
},
.crc = {},
};
std::array<u8, sizeof(IrsWriteRegisters)> request_data{};
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
do {
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return Common::Input::DriverResult::WrongReply;
}
} while (output.command_data[0] != 0x13 && output.command_data[0] != 0x23);
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult IrsProtocol::RequestFrame(u8 frame) {
std::array<u8, 38> mcu_request{};
mcu_request[3] = frame;
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
mcu_request[37] = 0xFF;
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
}
Common::Input::DriverResult IrsProtocol::ResendFrame(u8 frame) {
std::array<u8, 38> mcu_request{};
mcu_request[1] = 0x1;
mcu_request[2] = frame;
mcu_request[3] = 0x0;
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
mcu_request[37] = 0xFF;
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
}
std::vector<u8> IrsProtocol::GetImage() const {
return buf_image;
}
IrsResolution IrsProtocol::GetIrsFormat() const {
return resolution;
}
bool IrsProtocol::IsEnabled() const {
return is_enabled;
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace Common::Input {
enum class DriverResult;
}
namespace InputCommon::Joycon {
class IrsProtocol final : private JoyconCommonProtocol {
public:
explicit IrsProtocol(std::shared_ptr<JoyconHandle> handle);
Common::Input::DriverResult EnableIrs();
Common::Input::DriverResult DisableIrs();
Common::Input::DriverResult SetIrsConfig(IrsMode mode, IrsResolution format);
Common::Input::DriverResult RequestImage(std::span<u8> buffer);
std::vector<u8> GetImage() const;
IrsResolution GetIrsFormat() const;
bool IsEnabled() const;
private:
Common::Input::DriverResult ConfigureIrs();
Common::Input::DriverResult WriteRegistersStep1();
Common::Input::DriverResult WriteRegistersStep2();
Common::Input::DriverResult RequestFrame(u8 frame);
Common::Input::DriverResult ResendFrame(u8 frame);
IrsMode irs_mode{IrsMode::ImageTransfer};
IrsResolution resolution{IrsResolution::Size40x30};
IrsResolutionCode resolution_code{IrsResolutionCode::Size40x30};
IrsFragments fragments{IrsFragments::Size40x30};
IrLeds leds{IrLeds::BrightAndDim};
IrExLedFilter led_filter{IrExLedFilter::Enabled};
IrImageFlip image_flip{IrImageFlip::Normal};
u8 digital_gain{0x01};
u16 exposure{0x2490};
u16 led_intensity{0x0f10};
u32 denoise{0x012344};
u8 packet_fragment{};
std::vector<u8> buf_image; // 8bpp greyscale image.
bool is_enabled{};
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,791 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <array>
#include <functional>
#include <SDL_hidapi.h>
#include "common/bit_field.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
namespace InputCommon::Joycon {
constexpr u32 MaxErrorCount = 50;
constexpr u32 MaxBufferSize = 368;
constexpr std::array<u8, 8> DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40};
using MacAddress = std::array<u8, 6>;
using SerialNumber = std::array<u8, 15>;
using TagUUID = std::array<u8, 7>;
using MifareUUID = std::array<u8, 4>;
enum class ControllerType : u8 {
None = 0x00,
Left = 0x01,
Right = 0x02,
Pro = 0x03,
Dual = 0x05, // TODO: Verify this id
LarkHvc1 = 0x07,
LarkHvc2 = 0x08,
LarkNesLeft = 0x09,
LarkNesRight = 0x0A,
Lucia = 0x0B,
Lagon = 0x0C,
Lager = 0x0D,
};
enum class PadAxes {
LeftStickX,
LeftStickY,
RightStickX,
RightStickY,
Undefined,
};
enum class PadMotion {
LeftMotion,
RightMotion,
Undefined,
};
enum class PadButton : u32 {
Down = 0x000001,
Up = 0x000002,
Right = 0x000004,
Left = 0x000008,
LeftSR = 0x000010,
LeftSL = 0x000020,
L = 0x000040,
ZL = 0x000080,
Y = 0x000100,
X = 0x000200,
B = 0x000400,
A = 0x000800,
RightSR = 0x001000,
RightSL = 0x002000,
R = 0x004000,
ZR = 0x008000,
Minus = 0x010000,
Plus = 0x020000,
StickR = 0x040000,
StickL = 0x080000,
Home = 0x100000,
Capture = 0x200000,
};
enum class PassivePadButton : u32 {
Down_A = 0x0001,
Right_X = 0x0002,
Left_B = 0x0004,
Up_Y = 0x0008,
SL = 0x0010,
SR = 0x0020,
Minus = 0x0100,
Plus = 0x0200,
StickL = 0x0400,
StickR = 0x0800,
Home = 0x1000,
Capture = 0x2000,
L_R = 0x4000,
ZL_ZR = 0x8000,
};
enum class PassivePadStick : u8 {
Right = 0x00,
RightDown = 0x01,
Down = 0x02,
DownLeft = 0x03,
Left = 0x04,
LeftUp = 0x05,
Up = 0x06,
UpRight = 0x07,
Neutral = 0x08,
};
enum class OutputReport : u8 {
RUMBLE_AND_SUBCMD = 0x01,
FW_UPDATE_PKT = 0x03,
RUMBLE_ONLY = 0x10,
MCU_DATA = 0x11,
USB_CMD = 0x80,
};
enum class FeatureReport : u8 {
Last_SUBCMD = 0x02,
OTA_GW_UPGRADE = 0x70,
SETUP_MEM_READ = 0x71,
MEM_READ = 0x72,
ERASE_MEM_SECTOR = 0x73,
MEM_WRITE = 0x74,
LAUNCH = 0x75,
};
enum class SubCommand : u8 {
STATE = 0x00,
MANUAL_BT_PAIRING = 0x01,
REQ_DEV_INFO = 0x02,
SET_REPORT_MODE = 0x03,
TRIGGERS_ELAPSED = 0x04,
GET_PAGE_LIST_STATE = 0x05,
SET_HCI_STATE = 0x06,
RESET_PAIRING_INFO = 0x07,
LOW_POWER_MODE = 0x08,
SPI_FLASH_READ = 0x10,
SPI_FLASH_WRITE = 0x11,
SPI_SECTOR_ERASE = 0x12,
RESET_MCU = 0x20,
SET_MCU_CONFIG = 0x21,
SET_MCU_STATE = 0x22,
SET_PLAYER_LIGHTS = 0x30,
GET_PLAYER_LIGHTS = 0x31,
SET_HOME_LIGHT = 0x38,
ENABLE_IMU = 0x40,
SET_IMU_SENSITIVITY = 0x41,
WRITE_IMU_REG = 0x42,
READ_IMU_REG = 0x43,
ENABLE_VIBRATION = 0x48,
GET_REGULATED_VOLTAGE = 0x50,
SET_EXTERNAL_CONFIG = 0x58,
GET_EXTERNAL_DEVICE_INFO = 0x59,
ENABLE_EXTERNAL_POLLING = 0x5A,
DISABLE_EXTERNAL_POLLING = 0x5B,
SET_EXTERNAL_FORMAT_CONFIG = 0x5C,
};
enum class UsbSubCommand : u8 {
CONN_STATUS = 0x01,
HADSHAKE = 0x02,
BAUDRATE_3M = 0x03,
NO_TIMEOUT = 0x04,
EN_TIMEOUT = 0x05,
RESET = 0x06,
PRE_HANDSHAKE = 0x91,
SEND_UART = 0x92,
};
enum class CalibrationMagic : u8 {
USR_MAGIC_0 = 0xB2,
USR_MAGIC_1 = 0xA1,
};
enum class SpiAddress : u16 {
MAGIC = 0x0000,
MAC_ADDRESS = 0x0015,
PAIRING_INFO = 0x2000,
SHIPMENT = 0x5000,
SERIAL_NUMBER = 0x6000,
DEVICE_TYPE = 0x6012,
FORMAT_VERSION = 0x601B,
FACT_IMU_DATA = 0x6020,
FACT_LEFT_DATA = 0x603d,
FACT_RIGHT_DATA = 0x6046,
COLOR_DATA = 0x6050,
DESIGN_VARIATION = 0x605C,
SENSOR_DATA = 0x6080,
USER_LEFT_MAGIC = 0x8010,
USER_LEFT_DATA = 0x8012,
USER_RIGHT_MAGIC = 0x801B,
USER_RIGHT_DATA = 0x801D,
USER_IMU_MAGIC = 0x8026,
USER_IMU_DATA = 0x8028,
};
enum class ReportMode : u8 {
ACTIVE_POLLING_NFC_IR_CAMERA_DATA = 0x00,
ACTIVE_POLLING_NFC_IR_CAMERA_CONFIGURATION = 0x01,
ACTIVE_POLLING_NFC_IR_CAMERA_DATA_CONFIGURATION = 0x02,
ACTIVE_POLLING_IR_CAMERA_DATA = 0x03,
SUBCMD_REPLY = 0x21,
MCU_UPDATE_STATE = 0x23,
STANDARD_FULL_60HZ = 0x30,
NFC_IR_MODE_60HZ = 0x31,
SIMPLE_HID_MODE = 0x3F,
INPUT_USB_RESPONSE = 0x81,
};
enum class GyroSensitivity : u8 {
DPS250,
DPS500,
DPS1000,
DPS2000, // Default
};
enum class AccelerometerSensitivity : u8 {
G8, // Default
G4,
G2,
G16,
};
enum class GyroPerformance : u8 {
HZ833,
HZ208, // Default
};
enum class AccelerometerPerformance : u8 {
HZ200,
HZ100, // Default
};
enum class MCUCommand : u8 {
ConfigureMCU = 0x21,
ConfigureIR = 0x23,
};
enum class MCUSubCommand : u8 {
SetMCUMode = 0x0,
SetDeviceMode = 0x1,
ReadDeviceMode = 0x02,
WriteDeviceRegisters = 0x4,
};
enum class MCUMode : u8 {
Suspend = 0,
Standby = 1,
Ringcon = 3,
NFC = 4,
IR = 5,
MaybeFWUpdate = 6,
};
enum class MCURequest : u8 {
GetMCUStatus = 1,
GetNFCData = 2,
GetIRData = 3,
};
enum class MCUReport : u8 {
Empty = 0x00,
StateReport = 0x01,
IRData = 0x03,
BusyInitializing = 0x0b,
IRStatus = 0x13,
IRRegisters = 0x1b,
NFCState = 0x2a,
NFCReadData = 0x3a,
EmptyAwaitingCmd = 0xff,
};
enum class MCUPacketFlag : u8 {
MorePacketsRemaining = 0x00,
LastCommandPacket = 0x08,
};
enum class NFCCommand : u8 {
CancelAll = 0x00,
StartPolling = 0x01,
StopPolling = 0x02,
StartWaitingReceive = 0x04,
ReadNtag = 0x06,
WriteNtag = 0x08,
Mifare = 0x0F,
};
enum class NFCTagType : u8 {
AllTags = 0x00,
Ntag215 = 0x01,
};
enum class NFCPages {
Block0 = 0,
Block3 = 3,
Block45 = 45,
Block135 = 135,
Block231 = 231,
};
enum class NFCStatus : u8 {
Ready = 0x00,
Polling = 0x01,
LastPackage = 0x04,
WriteDone = 0x05,
TagLost = 0x07,
WriteReady = 0x09,
MifareDone = 0x10,
};
enum class MifareCmd : u8 {
None = 0x00,
Read = 0x30,
AuthA = 0x60,
AuthB = 0x61,
Write = 0xA0,
Transfer = 0xB0,
Decrement = 0xC0,
Increment = 0xC1,
Store = 0xC2
};
enum class IrsMode : u8 {
None = 0x02,
Moment = 0x03,
Dpd = 0x04,
Clustering = 0x06,
ImageTransfer = 0x07,
Silhouette = 0x08,
TeraImage = 0x09,
SilhouetteTeraImage = 0x0A,
};
enum class IrsResolution {
Size320x240,
Size160x120,
Size80x60,
Size40x30,
Size20x15,
None,
};
enum class IrsResolutionCode : u8 {
Size320x240 = 0x00, // Full pixel array
Size160x120 = 0x50, // Sensor Binning [2 X 2]
Size80x60 = 0x64, // Sensor Binning [4 x 2] and Skipping [1 x 2]
Size40x30 = 0x69, // Sensor Binning [4 x 2] and Skipping [2 x 4]
Size20x15 = 0x6A, // Sensor Binning [4 x 2] and Skipping [4 x 4]
};
// Size of image divided by 300
enum class IrsFragments : u8 {
Size20x15 = 0x00,
Size40x30 = 0x03,
Size80x60 = 0x0f,
Size160x120 = 0x3f,
Size320x240 = 0xFF,
};
enum class IrLeds : u8 {
BrightAndDim = 0x00,
Bright = 0x20,
Dim = 0x10,
None = 0x30,
};
enum class IrExLedFilter : u8 {
Disabled = 0x00,
Enabled = 0x03,
};
enum class IrImageFlip : u8 {
Normal = 0x00,
Inverted = 0x02,
};
enum class IrRegistersAddress : u16 {
UpdateTime = 0x0400,
FinalizeConfig = 0x0700,
LedFilter = 0x0e00,
Leds = 0x1000,
LedIntensityMSB = 0x1100,
LedIntensityLSB = 0x1200,
ImageFlip = 0x2d00,
Resolution = 0x2e00,
DigitalGainLSB = 0x2e01,
DigitalGainMSB = 0x2f01,
ExposureLSB = 0x3001,
ExposureMSB = 0x3101,
ExposureTime = 0x3201,
WhitePixelThreshold = 0x4301,
DenoiseSmoothing = 0x6701,
DenoiseEdge = 0x6801,
DenoiseColor = 0x6901,
};
enum class ExternalDeviceId : u16 {
RingController = 0x2000,
Starlink = 0x2800,
};
struct MotionSensorCalibration {
s16 offset;
s16 scale;
};
struct MotionCalibration {
std::array<MotionSensorCalibration, 3> accelerometer;
std::array<MotionSensorCalibration, 3> gyro;
};
// Basic motion data containing data from the sensors and a timestamp in microseconds
struct MotionData {
float gyro_x{};
float gyro_y{};
float gyro_z{};
float accel_x{};
float accel_y{};
float accel_z{};
u64 delta_timestamp{};
};
// Output from SPI read command containing user calibration magic
struct MagicSpiCalibration {
CalibrationMagic first;
CalibrationMagic second;
};
static_assert(sizeof(MagicSpiCalibration) == 0x2, "MagicSpiCalibration is an invalid size");
// Output from SPI read command containing left joystick calibration
struct JoystickLeftSpiCalibration {
std::array<u8, 3> max;
std::array<u8, 3> center;
std::array<u8, 3> min;
};
static_assert(sizeof(JoystickLeftSpiCalibration) == 0x9,
"JoystickLeftSpiCalibration is an invalid size");
// Output from SPI read command containing right joystick calibration
struct JoystickRightSpiCalibration {
std::array<u8, 3> center;
std::array<u8, 3> min;
std::array<u8, 3> max;
};
static_assert(sizeof(JoystickRightSpiCalibration) == 0x9,
"JoystickRightSpiCalibration is an invalid size");
struct JoyStickAxisCalibration {
u16 max;
u16 min;
u16 center;
};
struct JoyStickCalibration {
JoyStickAxisCalibration x;
JoyStickAxisCalibration y;
};
struct ImuSpiCalibration {
std::array<s16, 3> accelerometer_offset;
std::array<s16, 3> accelerometer_scale;
std::array<s16, 3> gyroscope_offset;
std::array<s16, 3> gyroscope_scale;
};
static_assert(sizeof(ImuSpiCalibration) == 0x18, "ImuSpiCalibration is an invalid size");
struct RingCalibration {
s16 default_value;
s16 max_value;
s16 min_value;
};
struct Color {
u32 body;
u32 buttons;
u32 left_grip;
u32 right_grip;
};
struct Battery {
union {
u8 raw{};
BitField<0, 4, u8> unknown;
BitField<4, 1, u8> charging;
BitField<5, 3, u8> status;
};
};
struct VibrationValue {
f32 low_amplitude;
f32 low_frequency;
f32 high_amplitude;
f32 high_frequency;
};
struct JoyconHandle {
SDL_hid_device* handle = nullptr;
u8 packet_counter{};
};
struct MCUConfig {
MCUCommand command;
MCUSubCommand sub_command;
MCUMode mode;
INSERT_PADDING_BYTES(0x22);
u8 crc;
};
static_assert(sizeof(MCUConfig) == 0x26, "MCUConfig is an invalid size");
#pragma pack(push, 1)
struct InputReportPassive {
ReportMode report_mode;
u16 button_input;
u8 stick_state;
std::array<u8, 10> unknown_data;
};
static_assert(sizeof(InputReportPassive) == 0xE, "InputReportPassive is an invalid size");
struct InputReportActive {
ReportMode report_mode;
u8 packet_id;
Battery battery_status;
std::array<u8, 3> button_input;
std::array<u8, 3> left_stick_state;
std::array<u8, 3> right_stick_state;
u8 vibration_code;
std::array<s16, 6 * 2> motion_input;
INSERT_PADDING_BYTES(0x2);
s16 ring_input;
};
static_assert(sizeof(InputReportActive) == 0x29, "InputReportActive is an invalid size");
struct InputReportNfcIr {
ReportMode report_mode;
u8 packet_id;
Battery battery_status;
std::array<u8, 3> button_input;
std::array<u8, 3> left_stick_state;
std::array<u8, 3> right_stick_state;
u8 vibration_code;
std::array<s16, 6 * 2> motion_input;
INSERT_PADDING_BYTES(0x4);
};
static_assert(sizeof(InputReportNfcIr) == 0x29, "InputReportNfcIr is an invalid size");
#pragma pack(pop)
struct NFCReadBlock {
u8 start;
u8 end;
};
static_assert(sizeof(NFCReadBlock) == 0x2, "NFCReadBlock is an invalid size");
struct NFCReadBlockCommand {
u8 block_count{};
std::array<NFCReadBlock, 4> blocks{};
};
static_assert(sizeof(NFCReadBlockCommand) == 0x9, "NFCReadBlockCommand is an invalid size");
struct NFCReadCommandData {
u8 unknown;
u8 uuid_length;
TagUUID uid;
NFCTagType tag_type;
NFCReadBlockCommand read_block;
};
static_assert(sizeof(NFCReadCommandData) == 0x13, "NFCReadCommandData is an invalid size");
#pragma pack(push, 1)
struct NFCWriteCommandData {
u8 unknown;
u8 uuid_length;
TagUUID uid;
NFCTagType tag_type;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 unknown5;
u8 unknown6;
u8 unknown7;
u8 unknown8;
u8 magic;
u16_be write_count;
u8 amiibo_version;
};
static_assert(sizeof(NFCWriteCommandData) == 0x15, "NFCWriteCommandData is an invalid size");
#pragma pack(pop)
struct MifareCommandData {
u8 unknown1;
u8 unknown2;
u8 number_of_short_bytes;
MifareUUID uid;
};
static_assert(sizeof(MifareCommandData) == 0x7, "MifareCommandData is an invalid size");
struct NFCPollingCommandData {
u8 enable_mifare;
u8 unknown_1;
u8 unknown_2;
u8 unknown_3;
u8 unknown_4;
};
static_assert(sizeof(NFCPollingCommandData) == 0x05, "NFCPollingCommandData is an invalid size");
struct NFCRequestState {
NFCCommand command_argument;
u8 block_id;
u8 packet_id;
MCUPacketFlag packet_flag;
u8 data_length;
union {
std::array<u8, 0x1F> raw_data;
NFCReadCommandData nfc_read;
NFCPollingCommandData nfc_polling;
};
u8 crc;
INSERT_PADDING_BYTES(0x1);
};
static_assert(sizeof(NFCRequestState) == 0x26, "NFCRequestState is an invalid size");
struct NFCDataChunk {
u8 nfc_page;
u8 data_size;
std::array<u8, 0xFF> data;
};
struct NFCWritePackage {
NFCWriteCommandData command_data;
u8 number_of_chunks;
std::array<NFCDataChunk, 4> data_chunks;
};
struct MifareReadChunk {
MifareCmd command;
std::array<u8, 0x6> sector_key;
u8 sector;
};
struct MifareWriteChunk {
MifareCmd command;
std::array<u8, 0x6> sector_key;
u8 sector;
std::array<u8, 0x10> data;
};
struct MifareReadData {
u8 sector;
std::array<u8, 0x10> data;
};
struct MifareReadPackage {
MifareCommandData command_data;
std::array<MifareReadChunk, 0x10> data_chunks;
};
struct MifareWritePackage {
MifareCommandData command_data;
std::array<MifareWriteChunk, 0x10> data_chunks;
};
struct TagInfo {
u8 uuid_length;
u8 protocol;
u8 tag_type;
std::array<u8, 10> uuid;
};
struct IrsConfigure {
MCUCommand command;
MCUSubCommand sub_command;
IrsMode irs_mode;
IrsFragments number_of_fragments;
u16 mcu_major_version;
u16 mcu_minor_version;
INSERT_PADDING_BYTES(0x1D);
u8 crc;
};
static_assert(sizeof(IrsConfigure) == 0x26, "IrsConfigure is an invalid size");
#pragma pack(push, 1)
struct IrsRegister {
IrRegistersAddress address;
u8 value;
};
static_assert(sizeof(IrsRegister) == 0x3, "IrsRegister is an invalid size");
struct IrsWriteRegisters {
MCUCommand command;
MCUSubCommand sub_command;
u8 number_of_registers;
std::array<IrsRegister, 9> registers;
INSERT_PADDING_BYTES(0x7);
u8 crc;
};
static_assert(sizeof(IrsWriteRegisters) == 0x26, "IrsWriteRegisters is an invalid size");
#pragma pack(pop)
struct FirmwareVersion {
u8 major;
u8 minor;
};
static_assert(sizeof(FirmwareVersion) == 0x2, "FirmwareVersion is an invalid size");
struct DeviceInfo {
FirmwareVersion firmware;
std::array<u8, 2> unknown_1;
MacAddress mac_address;
std::array<u8, 2> unknown_2;
};
static_assert(sizeof(DeviceInfo) == 0xC, "DeviceInfo is an invalid size");
struct MotionStatus {
bool is_enabled;
u64 delta_time;
GyroSensitivity gyro_sensitivity;
AccelerometerSensitivity accelerometer_sensitivity;
};
struct RingStatus {
bool is_enabled;
s16 default_value;
s16 max_value;
s16 min_value;
};
struct VibrationPacket {
OutputReport output_report;
u8 packet_counter;
std::array<u8, 0x8> vibration_data;
};
static_assert(sizeof(VibrationPacket) == 0xA, "VibrationPacket is an invalid size");
struct SubCommandPacket {
OutputReport output_report;
u8 packet_counter;
INSERT_PADDING_BYTES(0x8); // This contains vibration data
union {
SubCommand sub_command;
MCUSubCommand mcu_sub_command;
};
std::array<u8, 0x26> command_data;
};
static_assert(sizeof(SubCommandPacket) == 0x31, "SubCommandPacket is an invalid size");
#pragma pack(push, 1)
struct ReadSpiPacket {
SpiAddress spi_address;
INSERT_PADDING_BYTES(0x2);
u8 size;
};
static_assert(sizeof(ReadSpiPacket) == 0x5, "ReadSpiPacket is an invalid size");
struct SubCommandResponse {
InputReportPassive input_report;
SubCommand sub_command;
union {
std::array<u8, 0x30> command_data;
SpiAddress spi_address; // Reply from SPI_FLASH_READ subcommand
ExternalDeviceId external_device_id; // Reply from GET_EXTERNAL_DEVICE_INFO subcommand
DeviceInfo device_info; // Reply from REQ_DEV_INFO subcommand
};
u8 crc; // This is never used
};
static_assert(sizeof(SubCommandResponse) == 0x40, "SubCommandResponse is an invalid size");
#pragma pack(pop)
struct MCUCommandResponse {
InputReportNfcIr input_report;
INSERT_PADDING_BYTES(0x8);
MCUReport mcu_report;
std::array<u8, 0x13D> mcu_data;
u8 crc;
};
static_assert(sizeof(MCUCommandResponse) == 0x170, "MCUCommandResponse is an invalid size");
struct JoyconCallbacks {
std::function<void(Battery)> on_battery_data;
std::function<void(Color)> on_color_data;
std::function<void(int, bool)> on_button_data;
std::function<void(int, f32)> on_stick_data;
std::function<void(int, const MotionData&)> on_motion_data;
std::function<void(f32)> on_ring_data;
std::function<void(const Joycon::TagInfo&)> on_amiibo_data;
std::function<void(const std::vector<u8>&, IrsResolution)> on_camera_data;
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,992 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/input.h"
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/nfc.h"
namespace InputCommon::Joycon {
NfcProtocol::NfcProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
Common::Input::DriverResult NfcProtocol::EnableNfc() {
LOG_INFO(Input, "Enable NFC");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ);
}
if (result == Common::Input::DriverResult::Success) {
result = EnableMCU(true);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby);
}
if (result == Common::Input::DriverResult::Success) {
const MCUConfig config{
.command = MCUCommand::ConfigureMCU,
.sub_command = MCUSubCommand::SetMCUMode,
.mode = MCUMode::NFC,
.crc = {},
};
result = ConfigureMCU(config);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::NFC);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::Ready);
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStopPollingRequest(output);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::Ready);
}
if (result == Common::Input::DriverResult::Success) {
is_enabled = true;
}
return result;
}
Common::Input::DriverResult NfcProtocol::DisableNfc() {
LOG_DEBUG(Input, "Disable NFC");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
result = EnableMCU(false);
}
is_enabled = false;
is_polling = false;
return result;
}
Common::Input::DriverResult NfcProtocol::StartNFCPollingMode() {
LOG_DEBUG(Input, "Start NFC polling Mode");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStartPollingRequest(output);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::Polling);
}
if (result == Common::Input::DriverResult::Success) {
is_polling = true;
}
return result;
}
Common::Input::DriverResult NfcProtocol::StopNFCPollingMode() {
LOG_DEBUG(Input, "Stop NFC polling Mode");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStopPollingRequest(output);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::WriteReady);
}
if (result == Common::Input::DriverResult::Success) {
is_polling = false;
}
return result;
}
Common::Input::DriverResult NfcProtocol::GetTagInfo(Joycon::TagInfo& tag_info) {
if (update_counter++ < AMIIBO_UPDATE_DELAY) {
return Common::Input::DriverResult::Delayed;
}
update_counter = 0;
LOG_DEBUG(Input, "Scan for amiibos");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
TagFoundData tag_data{};
if (result == Common::Input::DriverResult::Success) {
result = IsTagInRange(tag_data);
}
if (result == Common::Input::DriverResult::Success) {
tag_info = {
.uuid_length = tag_data.uuid_size,
.protocol = 1,
.tag_type = tag_data.type,
.uuid = {},
};
memcpy(tag_info.uuid.data(), tag_data.uuid.data(), tag_data.uuid_size);
// Investigate why mifare type is not correct
if (tag_info.tag_type == 144) {
tag_info.tag_type = 1U << 6;
}
std::string uuid_string;
for (auto& content : tag_data.uuid) {
uuid_string += fmt::format(" {:02x}", content);
}
LOG_INFO(Input, "Tag detected, type={}, uuid={}", tag_data.type, uuid_string);
}
return result;
}
Common::Input::DriverResult NfcProtocol::ReadAmiibo(std::vector<u8>& data) {
LOG_DEBUG(Input, "Scan for amiibos");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
TagFoundData tag_data{};
if (result == Common::Input::DriverResult::Success) {
result = IsTagInRange(tag_data, 7);
}
if (result == Common::Input::DriverResult::Success) {
result = GetAmiiboData(data);
}
return result;
}
Common::Input::DriverResult NfcProtocol::WriteAmiibo(std::span<const u8> data) {
LOG_DEBUG(Input, "Write amiibo");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
TagUUID tag_uuid = GetTagUUID(data);
TagFoundData tag_data{};
if (result == Common::Input::DriverResult::Success) {
result = IsTagInRange(tag_data, 7);
}
if (result == Common::Input::DriverResult::Success) {
if (tag_data.uuid != tag_uuid) {
result = Common::Input::DriverResult::InvalidParameters;
}
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStopPollingRequest(output);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::Ready);
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStartPollingRequest(output, true);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::WriteReady);
}
if (result == Common::Input::DriverResult::Success) {
result = WriteAmiiboData(tag_uuid, data);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::WriteDone);
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStopPollingRequest(output);
}
return result;
}
Common::Input::DriverResult NfcProtocol::ReadMifare(std::span<const MifareReadChunk> read_request,
std::span<MifareReadData> out_data) {
LOG_DEBUG(Input, "Read mifare");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
TagFoundData tag_data{};
MifareUUID tag_uuid{};
if (result == Common::Input::DriverResult::Success) {
result = IsTagInRange(tag_data, 7);
}
if (result == Common::Input::DriverResult::Success) {
memcpy(tag_uuid.data(), tag_data.uuid.data(), sizeof(MifareUUID));
result = GetMifareData(tag_uuid, read_request, out_data);
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStopPollingRequest(output);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::Ready);
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStartPollingRequest(output, true);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::WriteReady);
}
return result;
}
Common::Input::DriverResult NfcProtocol::WriteMifare(
std::span<const MifareWriteChunk> write_request) {
LOG_DEBUG(Input, "Write mifare");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
TagFoundData tag_data{};
MifareUUID tag_uuid{};
if (result == Common::Input::DriverResult::Success) {
result = IsTagInRange(tag_data, 7);
}
if (result == Common::Input::DriverResult::Success) {
memcpy(tag_uuid.data(), tag_data.uuid.data(), sizeof(MifareUUID));
result = WriteMifareData(tag_uuid, write_request);
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStopPollingRequest(output);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::Ready);
}
if (result == Common::Input::DriverResult::Success) {
MCUCommandResponse output{};
result = SendStartPollingRequest(output, true);
}
if (result == Common::Input::DriverResult::Success) {
result = WaitUntilNfcIs(NFCStatus::WriteReady);
}
return result;
}
bool NfcProtocol::HasAmiibo() {
if (update_counter++ < AMIIBO_UPDATE_DELAY) {
return true;
}
update_counter = 0;
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
TagFoundData tag_data{};
if (result == Common::Input::DriverResult::Success) {
result = IsTagInRange(tag_data, 7);
}
return result == Common::Input::DriverResult::Success;
}
Common::Input::DriverResult NfcProtocol::WaitUntilNfcIs(NFCStatus status) {
constexpr std::size_t timeout_limit = 10;
MCUCommandResponse output{};
std::size_t tries = 0;
do {
auto result = SendNextPackageRequest(output, {});
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (tries++ > timeout_limit) {
return Common::Input::DriverResult::Timeout;
}
} while (output.mcu_report != MCUReport::NFCState ||
(output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
output.mcu_data[5] != 0x31 || output.mcu_data[6] != static_cast<u8>(status));
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult NfcProtocol::IsTagInRange(TagFoundData& data,
std::size_t timeout_limit) {
MCUCommandResponse output{};
std::size_t tries = 0;
do {
const auto result = SendNextPackageRequest(output, {});
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (tries++ > timeout_limit) {
return Common::Input::DriverResult::Timeout;
}
} while (output.mcu_report != MCUReport::NFCState ||
(output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
(output.mcu_data[6] != 0x09 && output.mcu_data[6] != 0x04));
data.type = output.mcu_data[12];
data.uuid_size = std::min(output.mcu_data[14], static_cast<u8>(sizeof(TagUUID)));
memcpy(data.uuid.data(), output.mcu_data.data() + 15, data.uuid.size());
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult NfcProtocol::GetAmiiboData(std::vector<u8>& ntag_data) {
constexpr std::size_t timeout_limit = 60;
MCUCommandResponse output{};
std::size_t tries = 0;
u8 package_index = 0;
std::size_t ntag_buffer_pos = 0;
auto result = SendReadAmiiboRequest(output, NFCPages::Block135);
if (result != Common::Input::DriverResult::Success) {
return result;
}
// Read Tag data
while (tries++ < timeout_limit) {
result = SendNextPackageRequest(output, package_index);
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if ((output.mcu_report == MCUReport::NFCReadData ||
output.mcu_report == MCUReport::NFCState) &&
nfc_status == NFCStatus::TagLost) {
return Common::Input::DriverResult::ErrorReadingData;
}
if (output.mcu_report == MCUReport::NFCReadData && output.mcu_data[1] == 0x07) {
std::size_t payload_size = (output.mcu_data[4] << 8 | output.mcu_data[5]) & 0x7FF;
if (output.mcu_data[2] == 0x01) {
memcpy(ntag_data.data() + ntag_buffer_pos, output.mcu_data.data() + 66,
payload_size - 60);
ntag_buffer_pos += payload_size - 60;
} else {
memcpy(ntag_data.data() + ntag_buffer_pos, output.mcu_data.data() + 6,
payload_size);
}
package_index++;
continue;
}
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) {
LOG_INFO(Input, "Finished reading amiibo");
return Common::Input::DriverResult::Success;
}
}
return Common::Input::DriverResult::Timeout;
}
Common::Input::DriverResult NfcProtocol::WriteAmiiboData(const TagUUID& tag_uuid,
std::span<const u8> data) {
constexpr std::size_t timeout_limit = 60;
const auto nfc_data = MakeAmiiboWritePackage(tag_uuid, data);
const std::vector<u8> nfc_buffer_data = SerializeWritePackage(nfc_data);
std::span<const u8> buffer(nfc_buffer_data);
MCUCommandResponse output{};
u8 block_id = 1;
u8 package_index = 0;
std::size_t tries = 0;
std::size_t current_position = 0;
LOG_INFO(Input, "Writing amiibo data");
auto result = SendWriteAmiiboRequest(output, tag_uuid);
if (result != Common::Input::DriverResult::Success) {
return result;
}
// Read Tag data but ignore the actual sent data
while (tries++ < timeout_limit) {
result = SendNextPackageRequest(output, package_index);
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if ((output.mcu_report == MCUReport::NFCReadData ||
output.mcu_report == MCUReport::NFCState) &&
nfc_status == NFCStatus::TagLost) {
return Common::Input::DriverResult::ErrorReadingData;
}
if (output.mcu_report == MCUReport::NFCReadData && output.mcu_data[1] == 0x07) {
package_index++;
continue;
}
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) {
LOG_INFO(Input, "Finished reading amiibo");
break;
}
}
// Send Data. Nfc buffer size is 31, Send the data in smaller packages
while (current_position < buffer.size() && tries++ < timeout_limit) {
const std::size_t next_position =
std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
const std::size_t block_size = next_position - current_position;
const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
SendWriteDataAmiiboRequest(output, block_id, is_last_packet,
buffer.subspan(current_position, block_size));
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
if ((output.mcu_report == MCUReport::NFCReadData ||
output.mcu_report == MCUReport::NFCState) &&
nfc_status == NFCStatus::TagLost) {
return Common::Input::DriverResult::ErrorReadingData;
}
// Increase position when data is confirmed by the joycon
if (output.mcu_report == MCUReport::NFCState &&
(output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
output.mcu_data[3] == block_id) {
block_id++;
current_position = next_position;
}
}
return result;
}
Common::Input::DriverResult NfcProtocol::GetMifareData(
const MifareUUID& tag_uuid, std::span<const MifareReadChunk> read_request,
std::span<MifareReadData> out_data) {
constexpr std::size_t timeout_limit = 60;
const auto nfc_data = MakeMifareReadPackage(tag_uuid, read_request);
const std::vector<u8> nfc_buffer_data = SerializeMifareReadPackage(nfc_data);
std::span<const u8> buffer(nfc_buffer_data);
Common::Input::DriverResult result = Common::Input::DriverResult::Success;
MCUCommandResponse output{};
u8 block_id = 1;
u8 package_index = 0;
std::size_t tries = 0;
std::size_t current_position = 0;
LOG_INFO(Input, "Reading Mifare data");
// Send data request. Nfc buffer size is 31, Send the data in smaller packages
while (current_position < buffer.size() && tries++ < timeout_limit) {
const std::size_t next_position =
std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
const std::size_t block_size = next_position - current_position;
const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
SendReadDataMifareRequest(output, block_id, is_last_packet,
buffer.subspan(current_position, block_size));
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
return Common::Input::DriverResult::ErrorReadingData;
}
// Increase position when data is confirmed by the joycon
if (output.mcu_report == MCUReport::NFCState &&
(output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
output.mcu_data[3] == block_id) {
block_id++;
current_position = next_position;
}
}
if (result != Common::Input::DriverResult::Success) {
return result;
}
// Wait for reply and save the output data
while (tries++ < timeout_limit) {
result = SendNextPackageRequest(output, package_index);
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
return Common::Input::DriverResult::ErrorReadingData;
}
if (output.mcu_report == MCUReport::NFCState && output.mcu_data[1] == 0x10) {
constexpr std::size_t DATA_LENGTH = 0x10 + 1;
constexpr std::size_t DATA_START = 11;
const u8 number_of_elements = output.mcu_data[10];
for (std::size_t i = 0; i < number_of_elements; i++) {
out_data[i].sector = output.mcu_data[DATA_START + (i * DATA_LENGTH)];
memcpy(out_data[i].data.data(),
output.mcu_data.data() + DATA_START + 1 + (i * DATA_LENGTH),
sizeof(MifareReadData::data));
}
package_index++;
continue;
}
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::MifareDone) {
LOG_INFO(Input, "Finished reading mifare");
break;
}
}
return result;
}
Common::Input::DriverResult NfcProtocol::WriteMifareData(
const MifareUUID& tag_uuid, std::span<const MifareWriteChunk> write_request) {
constexpr std::size_t timeout_limit = 60;
const auto nfc_data = MakeMifareWritePackage(tag_uuid, write_request);
const std::vector<u8> nfc_buffer_data = SerializeMifareWritePackage(nfc_data);
std::span<const u8> buffer(nfc_buffer_data);
Common::Input::DriverResult result = Common::Input::DriverResult::Success;
MCUCommandResponse output{};
u8 block_id = 1;
u8 package_index = 0;
std::size_t tries = 0;
std::size_t current_position = 0;
LOG_INFO(Input, "Writing Mifare data");
// Send data request. Nfc buffer size is 31, Send the data in smaller packages
while (current_position < buffer.size() && tries++ < timeout_limit) {
const std::size_t next_position =
std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
const std::size_t block_size = next_position - current_position;
const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
SendReadDataMifareRequest(output, block_id, is_last_packet,
buffer.subspan(current_position, block_size));
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
return Common::Input::DriverResult::ErrorReadingData;
}
// Increase position when data is confirmed by the joycon
if (output.mcu_report == MCUReport::NFCState &&
(output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
output.mcu_data[3] == block_id) {
block_id++;
current_position = next_position;
}
}
if (result != Common::Input::DriverResult::Success) {
return result;
}
// Wait for reply and ignore the output data
while (tries++ < timeout_limit) {
result = SendNextPackageRequest(output, package_index);
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
if (result != Common::Input::DriverResult::Success) {
return result;
}
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
return Common::Input::DriverResult::ErrorReadingData;
}
if (output.mcu_report == MCUReport::NFCState && output.mcu_data[1] == 0x10) {
package_index++;
continue;
}
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::MifareDone) {
LOG_INFO(Input, "Finished writing mifare");
break;
}
}
return result;
}
Common::Input::DriverResult NfcProtocol::SendStartPollingRequest(MCUCommandResponse& output,
bool is_second_attempt) {
NFCRequestState request{
.command_argument = NFCCommand::StartPolling,
.block_id = {},
.packet_id = {},
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = sizeof(NFCPollingCommandData),
.nfc_polling =
{
.enable_mifare = 0x00,
.unknown_1 = static_cast<u8>(is_second_attempt ? 0xe8 : 0x00),
.unknown_2 = static_cast<u8>(is_second_attempt ? 0x03 : 0x00),
.unknown_3 = 0x2c,
.unknown_4 = 0x01,
},
.crc = {},
};
std::array<u8, sizeof(NFCRequestState)> request_data{};
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
output);
}
Common::Input::DriverResult NfcProtocol::SendStopPollingRequest(MCUCommandResponse& output) {
NFCRequestState request{
.command_argument = NFCCommand::StopPolling,
.block_id = {},
.packet_id = {},
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = {},
.raw_data = {},
.crc = {},
};
std::array<u8, sizeof(NFCRequestState)> request_data{};
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
output);
}
Common::Input::DriverResult NfcProtocol::SendNextPackageRequest(MCUCommandResponse& output,
u8 packet_id) {
NFCRequestState request{
.command_argument = NFCCommand::StartWaitingReceive,
.block_id = {},
.packet_id = packet_id,
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = {},
.raw_data = {},
.crc = {},
};
std::vector<u8> request_data(sizeof(NFCRequestState));
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
output);
}
Common::Input::DriverResult NfcProtocol::SendReadAmiiboRequest(MCUCommandResponse& output,
NFCPages ntag_pages) {
NFCRequestState request{
.command_argument = NFCCommand::ReadNtag,
.block_id = {},
.packet_id = {},
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = sizeof(NFCReadCommandData),
.nfc_read =
{
.unknown = 0xd0,
.uuid_length = sizeof(NFCReadCommandData::uid),
.uid = {},
.tag_type = NFCTagType::Ntag215,
.read_block = GetReadBlockCommand(ntag_pages),
},
.crc = {},
};
std::array<u8, sizeof(NFCRequestState)> request_data{};
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
output);
}
Common::Input::DriverResult NfcProtocol::SendWriteAmiiboRequest(MCUCommandResponse& output,
const TagUUID& tag_uuid) {
NFCRequestState request{
.command_argument = NFCCommand::ReadNtag,
.block_id = {},
.packet_id = {},
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = sizeof(NFCReadCommandData),
.nfc_read =
{
.unknown = 0xd0,
.uuid_length = sizeof(NFCReadCommandData::uid),
.uid = tag_uuid,
.tag_type = NFCTagType::Ntag215,
.read_block = GetReadBlockCommand(NFCPages::Block3),
},
.crc = {},
};
std::array<u8, sizeof(NFCRequestState)> request_data{};
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
output);
}
Common::Input::DriverResult NfcProtocol::SendWriteDataAmiiboRequest(MCUCommandResponse& output,
u8 block_id,
bool is_last_packet,
std::span<const u8> data) {
const auto data_size = std::min(data.size(), sizeof(NFCRequestState::raw_data));
NFCRequestState request{
.command_argument = NFCCommand::WriteNtag,
.block_id = block_id,
.packet_id = {},
.packet_flag =
is_last_packet ? MCUPacketFlag::LastCommandPacket : MCUPacketFlag::MorePacketsRemaining,
.data_length = static_cast<u8>(data_size),
.raw_data = {},
.crc = {},
};
memcpy(request.raw_data.data(), data.data(), data_size);
std::array<u8, sizeof(NFCRequestState)> request_data{};
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
output);
}
Common::Input::DriverResult NfcProtocol::SendReadDataMifareRequest(MCUCommandResponse& output,
u8 block_id, bool is_last_packet,
std::span<const u8> data) {
const auto data_size = std::min(data.size(), sizeof(NFCRequestState::raw_data));
NFCRequestState request{
.command_argument = NFCCommand::Mifare,
.block_id = block_id,
.packet_id = {},
.packet_flag =
is_last_packet ? MCUPacketFlag::LastCommandPacket : MCUPacketFlag::MorePacketsRemaining,
.data_length = static_cast<u8>(data_size),
.raw_data = {},
.crc = {},
};
memcpy(request.raw_data.data(), data.data(), data_size);
std::array<u8, sizeof(NFCRequestState)> request_data{};
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
output);
}
std::vector<u8> NfcProtocol::SerializeWritePackage(const NFCWritePackage& package) const {
const std::size_t header_size =
sizeof(NFCWriteCommandData) + sizeof(NFCWritePackage::number_of_chunks);
std::vector<u8> serialized_data(header_size);
std::size_t start_index = 0;
memcpy(serialized_data.data(), &package, header_size);
start_index += header_size;
for (const auto& data_chunk : package.data_chunks) {
const std::size_t chunk_size =
sizeof(NFCDataChunk::nfc_page) + sizeof(NFCDataChunk::data_size) + data_chunk.data_size;
serialized_data.resize(start_index + chunk_size);
memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
start_index += chunk_size;
}
return serialized_data;
}
std::vector<u8> NfcProtocol::SerializeMifareReadPackage(const MifareReadPackage& package) const {
const std::size_t header_size = sizeof(MifareCommandData);
std::vector<u8> serialized_data(header_size);
std::size_t start_index = 0;
memcpy(serialized_data.data(), &package, header_size);
start_index += header_size;
for (const auto& data_chunk : package.data_chunks) {
const std::size_t chunk_size = sizeof(MifareReadChunk);
if (data_chunk.command == MifareCmd::None) {
continue;
}
serialized_data.resize(start_index + chunk_size);
memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
start_index += chunk_size;
}
return serialized_data;
}
std::vector<u8> NfcProtocol::SerializeMifareWritePackage(const MifareWritePackage& package) const {
const std::size_t header_size = sizeof(MifareCommandData);
std::vector<u8> serialized_data(header_size);
std::size_t start_index = 0;
memcpy(serialized_data.data(), &package, header_size);
start_index += header_size;
for (const auto& data_chunk : package.data_chunks) {
const std::size_t chunk_size = sizeof(MifareWriteChunk);
if (data_chunk.command == MifareCmd::None) {
continue;
}
serialized_data.resize(start_index + chunk_size);
memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
start_index += chunk_size;
}
return serialized_data;
}
NFCWritePackage NfcProtocol::MakeAmiiboWritePackage(const TagUUID& tag_uuid,
std::span<const u8> data) const {
return {
.command_data{
.unknown = 0xd0,
.uuid_length = sizeof(NFCReadCommandData::uid),
.uid = tag_uuid,
.tag_type = NFCTagType::Ntag215,
.unknown2 = 0x00,
.unknown3 = 0x01,
.unknown4 = 0x04,
.unknown5 = 0xff,
.unknown6 = 0xff,
.unknown7 = 0xff,
.unknown8 = 0xff,
.magic = data[16],
.write_count = static_cast<u16>((data[17] << 8) + data[18]),
.amiibo_version = data[19],
},
.number_of_chunks = 3,
.data_chunks =
{
MakeAmiiboChunk(0x05, 0x20, data),
MakeAmiiboChunk(0x20, 0xf0, data),
MakeAmiiboChunk(0x5c, 0x98, data),
},
};
}
MifareReadPackage NfcProtocol::MakeMifareReadPackage(
const MifareUUID& tag_uuid, std::span<const MifareReadChunk> read_request) const {
MifareReadPackage package{
.command_data{
.unknown1 = 0xd0,
.unknown2 = 0x07,
.number_of_short_bytes = static_cast<u8>(
((read_request.size() * sizeof(MifareReadChunk)) + sizeof(MifareUUID)) / 2),
.uid = tag_uuid,
},
.data_chunks = {},
};
for (std::size_t i = 0; i < read_request.size() && i < package.data_chunks.size(); ++i) {
package.data_chunks[i] = read_request[i];
}
return package;
}
MifareWritePackage NfcProtocol::MakeMifareWritePackage(
const MifareUUID& tag_uuid, std::span<const MifareWriteChunk> read_request) const {
MifareWritePackage package{
.command_data{
.unknown1 = 0xd0,
.unknown2 = 0x07,
.number_of_short_bytes = static_cast<u8>(
((read_request.size() * sizeof(MifareReadChunk)) + sizeof(MifareUUID) + 2) / 2),
.uid = tag_uuid,
},
.data_chunks = {},
};
for (std::size_t i = 0; i < read_request.size() && i < package.data_chunks.size(); ++i) {
package.data_chunks[i] = read_request[i];
}
return package;
}
NFCDataChunk NfcProtocol::MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const {
constexpr u8 NFC_PAGE_SIZE = 4;
if (static_cast<std::size_t>(page * NFC_PAGE_SIZE) + size >= data.size()) {
return {};
}
NFCDataChunk chunk{
.nfc_page = page,
.data_size = size,
.data = {},
};
std::memcpy(chunk.data.data(), data.data() + (page * NFC_PAGE_SIZE), size);
return chunk;
}
NFCReadBlockCommand NfcProtocol::GetReadBlockCommand(NFCPages pages) const {
switch (pages) {
case NFCPages::Block0:
return {
.block_count = 1,
};
case NFCPages::Block3:
return {
.block_count = 1,
.blocks =
{
NFCReadBlock{0x03, 0x03},
},
};
case NFCPages::Block45:
return {
.block_count = 1,
.blocks =
{
NFCReadBlock{0x00, 0x2C},
},
};
case NFCPages::Block135:
return {
.block_count = 3,
.blocks =
{
NFCReadBlock{0x00, 0x3b},
{0x3c, 0x77},
{0x78, 0x86},
},
};
case NFCPages::Block231:
return {
.block_count = 4,
.blocks =
{
NFCReadBlock{0x00, 0x3b},
{0x3c, 0x77},
{0x78, 0x83},
{0xb4, 0xe6},
},
};
default:
return {};
};
}
TagUUID NfcProtocol::GetTagUUID(std::span<const u8> data) const {
if (data.size() < 10) {
return {};
}
// crc byte 3 is omitted in this operation
return {
data[0], data[1], data[2], data[4], data[5], data[6], data[7],
};
}
bool NfcProtocol::IsEnabled() const {
return is_enabled;
}
bool NfcProtocol::IsPolling() const {
return is_polling;
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace Common::Input {
enum class DriverResult;
}
namespace InputCommon::Joycon {
class NfcProtocol final : private JoyconCommonProtocol {
public:
explicit NfcProtocol(std::shared_ptr<JoyconHandle> handle);
Common::Input::DriverResult EnableNfc();
Common::Input::DriverResult DisableNfc();
Common::Input::DriverResult StartNFCPollingMode();
Common::Input::DriverResult StopNFCPollingMode();
Common::Input::DriverResult GetTagInfo(Joycon::TagInfo& tag_info);
Common::Input::DriverResult ReadAmiibo(std::vector<u8>& data);
Common::Input::DriverResult WriteAmiibo(std::span<const u8> data);
Common::Input::DriverResult ReadMifare(std::span<const MifareReadChunk> read_request,
std::span<MifareReadData> out_data);
Common::Input::DriverResult WriteMifare(std::span<const MifareWriteChunk> write_request);
bool HasAmiibo();
bool IsEnabled() const;
bool IsPolling() const;
private:
// Number of times the function will be delayed until it outputs valid data
static constexpr std::size_t AMIIBO_UPDATE_DELAY = 15;
struct TagFoundData {
u8 type;
u8 uuid_size;
TagUUID uuid;
};
Common::Input::DriverResult WaitUntilNfcIs(NFCStatus status);
Common::Input::DriverResult IsTagInRange(TagFoundData& data, std::size_t timeout_limit = 1);
Common::Input::DriverResult GetAmiiboData(std::vector<u8>& data);
Common::Input::DriverResult WriteAmiiboData(const TagUUID& tag_uuid, std::span<const u8> data);
Common::Input::DriverResult GetMifareData(const MifareUUID& tag_uuid,
std::span<const MifareReadChunk> read_request,
std::span<MifareReadData> out_data);
Common::Input::DriverResult WriteMifareData(const MifareUUID& tag_uuid,
std::span<const MifareWriteChunk> write_request);
Common::Input::DriverResult SendStartPollingRequest(MCUCommandResponse& output,
bool is_second_attempt = false);
Common::Input::DriverResult SendStopPollingRequest(MCUCommandResponse& output);
Common::Input::DriverResult SendNextPackageRequest(MCUCommandResponse& output, u8 packet_id);
Common::Input::DriverResult SendReadAmiiboRequest(MCUCommandResponse& output,
NFCPages ntag_pages);
Common::Input::DriverResult SendWriteAmiiboRequest(MCUCommandResponse& output,
const TagUUID& tag_uuid);
Common::Input::DriverResult SendWriteDataAmiiboRequest(MCUCommandResponse& output, u8 block_id,
bool is_last_packet,
std::span<const u8> data);
Common::Input::DriverResult SendReadDataMifareRequest(MCUCommandResponse& output, u8 block_id,
bool is_last_packet,
std::span<const u8> data);
std::vector<u8> SerializeWritePackage(const NFCWritePackage& package) const;
std::vector<u8> SerializeMifareReadPackage(const MifareReadPackage& package) const;
std::vector<u8> SerializeMifareWritePackage(const MifareWritePackage& package) const;
NFCWritePackage MakeAmiiboWritePackage(const TagUUID& tag_uuid, std::span<const u8> data) const;
NFCDataChunk MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const;
MifareReadPackage MakeMifareReadPackage(const MifareUUID& tag_uuid,
std::span<const MifareReadChunk> read_request) const;
MifareWritePackage MakeMifareWritePackage(const MifareUUID& tag_uuid,
std::span<const MifareWriteChunk> read_request) const;
NFCReadBlockCommand GetReadBlockCommand(NFCPages pages) const;
TagUUID GetTagUUID(std::span<const u8> data) const;
bool is_enabled{};
bool is_polling{};
std::size_t update_counter{};
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,374 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/poller.h"
namespace InputCommon::Joycon {
JoyconPoller::JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_,
JoyStickCalibration right_stick_calibration_,
MotionCalibration motion_calibration_)
: device_type{device_type_}, left_stick_calibration{left_stick_calibration_},
right_stick_calibration{right_stick_calibration_}, motion_calibration{motion_calibration_} {}
void JoyconPoller::SetCallbacks(const JoyconCallbacks& callbacks_) {
callbacks = std::move(callbacks_);
}
void JoyconPoller::ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
const RingStatus& ring_status) {
InputReportActive data{};
memcpy(&data, buffer.data(), sizeof(InputReportActive));
switch (device_type) {
case ControllerType::Left:
UpdateActiveLeftPadInput(data, motion_status);
break;
case ControllerType::Right:
UpdateActiveRightPadInput(data, motion_status);
break;
case ControllerType::Pro:
UpdateActiveProPadInput(data, motion_status);
break;
default:
break;
}
if (ring_status.is_enabled) {
UpdateRing(data.ring_input, ring_status);
}
callbacks.on_battery_data(data.battery_status);
}
void JoyconPoller::ReadPassiveMode(std::span<u8> buffer) {
InputReportPassive data{};
memcpy(&data, buffer.data(), sizeof(InputReportPassive));
switch (device_type) {
case ControllerType::Left:
UpdatePassiveLeftPadInput(data);
break;
case ControllerType::Right:
UpdatePassiveRightPadInput(data);
break;
case ControllerType::Pro:
UpdatePassiveProPadInput(data);
break;
default:
break;
}
}
void JoyconPoller::ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status) {
// This mode is compatible with the active mode
ReadActiveMode(buffer, motion_status, {});
}
void JoyconPoller::UpdateColor(const Color& color) {
callbacks.on_color_data(color);
}
void JoyconPoller::UpdateAmiibo(const Joycon::TagInfo& tag_info) {
callbacks.on_amiibo_data(tag_info);
}
void JoyconPoller::UpdateCamera(const std::vector<u8>& camera_data, IrsResolution format) {
callbacks.on_camera_data(camera_data, format);
}
void JoyconPoller::UpdateRing(s16 value, const RingStatus& ring_status) {
float normalized_value = static_cast<float>(value - ring_status.default_value);
if (normalized_value > 0) {
normalized_value = normalized_value /
static_cast<float>(ring_status.max_value - ring_status.default_value);
}
if (normalized_value < 0) {
normalized_value = normalized_value /
static_cast<float>(ring_status.default_value - ring_status.min_value);
}
callbacks.on_ring_data(normalized_value);
}
void JoyconPoller::UpdateActiveLeftPadInput(const InputReportActive& input,
const MotionStatus& motion_status) {
static constexpr std::array<Joycon::PadButton, 11> left_buttons{
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
Joycon::PadButton::Left, Joycon::PadButton::LeftSL, Joycon::PadButton::LeftSR,
Joycon::PadButton::L, Joycon::PadButton::ZL, Joycon::PadButton::Minus,
Joycon::PadButton::Capture, Joycon::PadButton::StickL,
};
const u32 raw_button =
static_cast<u32>(input.button_input[2] | ((input.button_input[1] & 0b00101001) << 16));
for (std::size_t i = 0; i < left_buttons.size(); ++i) {
const bool button_status = (raw_button & static_cast<u32>(left_buttons[i])) != 0;
const int button = static_cast<int>(left_buttons[i]);
callbacks.on_button_data(button, button_status);
}
const u16 raw_left_axis_x =
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
const u16 raw_left_axis_y =
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
if (motion_status.is_enabled) {
auto left_motion = GetMotionInput(input, motion_status);
// Rotate motion axis to the correct direction
left_motion.accel_y = -left_motion.accel_y;
left_motion.accel_z = -left_motion.accel_z;
left_motion.gyro_x = -left_motion.gyro_x;
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), left_motion);
}
}
void JoyconPoller::UpdateActiveRightPadInput(const InputReportActive& input,
const MotionStatus& motion_status) {
static constexpr std::array<Joycon::PadButton, 11> right_buttons{
Joycon::PadButton::Y, Joycon::PadButton::X, Joycon::PadButton::B,
Joycon::PadButton::A, Joycon::PadButton::RightSL, Joycon::PadButton::RightSR,
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
Joycon::PadButton::Home, Joycon::PadButton::StickR,
};
const u32 raw_button =
static_cast<u32>((input.button_input[0] << 8) | (input.button_input[1] << 16));
for (std::size_t i = 0; i < right_buttons.size(); ++i) {
const bool button_status = (raw_button & static_cast<u32>(right_buttons[i])) != 0;
const int button = static_cast<int>(right_buttons[i]);
callbacks.on_button_data(button, button_status);
}
const u16 raw_right_axis_x =
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
const u16 raw_right_axis_y =
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
if (motion_status.is_enabled) {
auto right_motion = GetMotionInput(input, motion_status);
// Rotate motion axis to the correct direction
right_motion.accel_x = -right_motion.accel_x;
right_motion.accel_y = -right_motion.accel_y;
right_motion.gyro_z = -right_motion.gyro_z;
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), right_motion);
}
}
void JoyconPoller::UpdateActiveProPadInput(const InputReportActive& input,
const MotionStatus& motion_status) {
static constexpr std::array<Joycon::PadButton, 18> pro_buttons{
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
Joycon::PadButton::Left, Joycon::PadButton::L, Joycon::PadButton::ZL,
Joycon::PadButton::Minus, Joycon::PadButton::Capture, Joycon::PadButton::Y,
Joycon::PadButton::X, Joycon::PadButton::B, Joycon::PadButton::A,
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
Joycon::PadButton::Home, Joycon::PadButton::StickL, Joycon::PadButton::StickR,
};
const u32 raw_button = static_cast<u32>(input.button_input[2] | (input.button_input[0] << 8) |
(input.button_input[1] << 16));
for (std::size_t i = 0; i < pro_buttons.size(); ++i) {
const bool button_status = (raw_button & static_cast<u32>(pro_buttons[i])) != 0;
const int button = static_cast<int>(pro_buttons[i]);
callbacks.on_button_data(button, button_status);
}
const u16 raw_left_axis_x =
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
const u16 raw_left_axis_y =
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
const u16 raw_right_axis_x =
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
const u16 raw_right_axis_y =
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
if (motion_status.is_enabled) {
auto pro_motion = GetMotionInput(input, motion_status);
pro_motion.gyro_x = -pro_motion.gyro_x;
pro_motion.accel_y = -pro_motion.accel_y;
pro_motion.accel_z = -pro_motion.accel_z;
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), pro_motion);
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), pro_motion);
}
}
void JoyconPoller::UpdatePassiveLeftPadInput(const InputReportPassive& input) {
static constexpr std::array<PassivePadButton, 11> left_buttons{
PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Minus,
PassivePadButton::Capture, PassivePadButton::StickL,
};
for (auto left_button : left_buttons) {
const bool button_status = (input.button_input & static_cast<u32>(left_button)) != 0;
const int button = static_cast<int>(left_button);
callbacks.on_button_data(button, button_status);
}
const auto [left_axis_x, left_axis_y] =
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state));
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
}
void JoyconPoller::UpdatePassiveRightPadInput(const InputReportPassive& input) {
static constexpr std::array<PassivePadButton, 11> right_buttons{
PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Plus,
PassivePadButton::Home, PassivePadButton::StickR,
};
for (auto right_button : right_buttons) {
const bool button_status = (input.button_input & static_cast<u32>(right_button)) != 0;
const int button = static_cast<int>(right_button);
callbacks.on_button_data(button, button_status);
}
const auto [right_axis_x, right_axis_y] =
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state));
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
}
void JoyconPoller::UpdatePassiveProPadInput(const InputReportPassive& input) {
static constexpr std::array<PassivePadButton, 14> pro_buttons{
PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Minus,
PassivePadButton::Plus, PassivePadButton::Capture, PassivePadButton::Home,
PassivePadButton::StickL, PassivePadButton::StickR,
};
for (auto pro_button : pro_buttons) {
const bool button_status = (input.button_input & static_cast<u32>(pro_button)) != 0;
const int button = static_cast<int>(pro_button);
callbacks.on_button_data(button, button_status);
}
const auto [left_axis_x, left_axis_y] =
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state & 0xf));
const auto [right_axis_x, right_axis_y] =
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state >> 4));
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
}
f32 JoyconPoller::GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const {
const f32 value = static_cast<f32>(raw_value - calibration.center);
if (value > 0.0f) {
return value / calibration.max;
}
return value / calibration.min;
}
std::pair<f32, f32> JoyconPoller::GetPassiveAxisValue(PassivePadStick raw_value) const {
switch (raw_value) {
case PassivePadStick::Right:
return {1.0f, 0.0f};
case PassivePadStick::RightDown:
return {1.0f, -1.0f};
case PassivePadStick::Down:
return {0.0f, -1.0f};
case PassivePadStick::DownLeft:
return {-1.0f, -1.0f};
case PassivePadStick::Left:
return {-1.0f, 0.0f};
case PassivePadStick::LeftUp:
return {-1.0f, 1.0f};
case PassivePadStick::Up:
return {0.0f, 1.0f};
case PassivePadStick::UpRight:
return {1.0f, 1.0f};
case PassivePadStick::Neutral:
default:
return {0.0f, 0.0f};
}
}
f32 JoyconPoller::GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal,
AccelerometerSensitivity sensitivity) const {
const f32 value = raw * (1.0f / (cal.scale - cal.offset)) * 4;
switch (sensitivity) {
case Joycon::AccelerometerSensitivity::G2:
return value / 4.0f;
case Joycon::AccelerometerSensitivity::G4:
return value / 2.0f;
case Joycon::AccelerometerSensitivity::G8:
return value;
case Joycon::AccelerometerSensitivity::G16:
return value * 2.0f;
}
return value;
}
f32 JoyconPoller::GetGyroValue(s16 raw, const MotionSensorCalibration& cal,
GyroSensitivity sensitivity) const {
const f32 value = (raw - cal.offset) * (936.0f / (cal.scale - cal.offset)) / 360.0f;
switch (sensitivity) {
case Joycon::GyroSensitivity::DPS250:
return value / 8.0f;
case Joycon::GyroSensitivity::DPS500:
return value / 4.0f;
case Joycon::GyroSensitivity::DPS1000:
return value / 2.0f;
case Joycon::GyroSensitivity::DPS2000:
return value;
}
return value;
}
s16 JoyconPoller::GetRawIMUValues(std::size_t sensor, size_t axis,
const InputReportActive& input) const {
return input.motion_input[(sensor * 3) + axis];
}
MotionData JoyconPoller::GetMotionInput(const InputReportActive& input,
const MotionStatus& motion_status) const {
MotionData motion{};
const auto& accel_cal = motion_calibration.accelerometer;
const auto& gyro_cal = motion_calibration.gyro;
const s16 raw_accel_x = input.motion_input[1];
const s16 raw_accel_y = input.motion_input[0];
const s16 raw_accel_z = input.motion_input[2];
const s16 raw_gyro_x = input.motion_input[4];
const s16 raw_gyro_y = input.motion_input[3];
const s16 raw_gyro_z = input.motion_input[5];
motion.delta_timestamp = motion_status.delta_time;
motion.accel_x =
GetAccelerometerValue(raw_accel_x, accel_cal[1], motion_status.accelerometer_sensitivity);
motion.accel_y =
GetAccelerometerValue(raw_accel_y, accel_cal[0], motion_status.accelerometer_sensitivity);
motion.accel_z =
GetAccelerometerValue(raw_accel_z, accel_cal[2], motion_status.accelerometer_sensitivity);
motion.gyro_x = GetGyroValue(raw_gyro_x, gyro_cal[1], motion_status.gyro_sensitivity);
motion.gyro_y = GetGyroValue(raw_gyro_y, gyro_cal[0], motion_status.gyro_sensitivity);
motion.gyro_z = GetGyroValue(raw_gyro_z, gyro_cal[2], motion_status.gyro_sensitivity);
// TODO(German77): Return all three samples data
return motion;
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <functional>
#include <span>
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
// Handles input packages and triggers the corresponding input events
class JoyconPoller {
public:
JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_,
JoyStickCalibration right_stick_calibration_,
MotionCalibration motion_calibration_);
void SetCallbacks(const JoyconCallbacks& callbacks_);
/// Handles data from passive packages
void ReadPassiveMode(std::span<u8> buffer);
/// Handles data from active packages
void ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
const RingStatus& ring_status);
/// Handles data from nfc or ir packages
void ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status);
void UpdateColor(const Color& color);
void UpdateRing(s16 value, const RingStatus& ring_status);
void UpdateAmiibo(const Joycon::TagInfo& tag_info);
void UpdateCamera(const std::vector<u8>& camera_data, IrsResolution format);
private:
void UpdateActiveLeftPadInput(const InputReportActive& input,
const MotionStatus& motion_status);
void UpdateActiveRightPadInput(const InputReportActive& input,
const MotionStatus& motion_status);
void UpdateActiveProPadInput(const InputReportActive& input, const MotionStatus& motion_status);
void UpdatePassiveLeftPadInput(const InputReportPassive& buffer);
void UpdatePassiveRightPadInput(const InputReportPassive& buffer);
void UpdatePassiveProPadInput(const InputReportPassive& buffer);
/// Returns a calibrated joystick axis from raw axis data
f32 GetAxisValue(u16 raw_value, JoyStickAxisCalibration calibration) const;
/// Returns a digital joystick axis from passive axis data
std::pair<f32, f32> GetPassiveAxisValue(PassivePadStick raw_value) const;
/// Returns a calibrated accelerometer axis from raw motion data
f32 GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal,
AccelerometerSensitivity sensitivity) const;
/// Returns a calibrated gyro axis from raw motion data
f32 GetGyroValue(s16 raw_value, const MotionSensorCalibration& cal,
GyroSensitivity sensitivity) const;
/// Returns a raw motion value from a buffer
s16 GetRawIMUValues(size_t sensor, size_t axis, const InputReportActive& input) const;
/// Returns motion data from a buffer
MotionData GetMotionInput(const InputReportActive& input,
const MotionStatus& motion_status) const;
ControllerType device_type{};
// Device calibration
JoyStickCalibration left_stick_calibration{};
JoyStickCalibration right_stick_calibration{};
MotionCalibration motion_calibration{};
JoyconCallbacks callbacks{};
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/input.h"
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/ringcon.h"
namespace InputCommon::Joycon {
RingConProtocol::RingConProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
Common::Input::DriverResult RingConProtocol::EnableRingCon() {
LOG_DEBUG(Input, "Enable Ringcon");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
result = SetReportMode(ReportMode::STANDARD_FULL_60HZ);
}
if (result == Common::Input::DriverResult::Success) {
result = EnableMCU(true);
}
if (result == Common::Input::DriverResult::Success) {
const MCUConfig config{
.command = MCUCommand::ConfigureMCU,
.sub_command = MCUSubCommand::SetDeviceMode,
.mode = MCUMode::Standby,
.crc = {},
};
result = ConfigureMCU(config);
}
return result;
}
Common::Input::DriverResult RingConProtocol::DisableRingCon() {
LOG_DEBUG(Input, "Disable RingCon");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
if (result == Common::Input::DriverResult::Success) {
result = EnableMCU(false);
}
is_enabled = false;
return result;
}
Common::Input::DriverResult RingConProtocol::StartRingconPolling() {
LOG_DEBUG(Input, "Enable Ringcon");
ScopedSetBlocking sb(this);
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
bool is_connected = false;
if (result == Common::Input::DriverResult::Success) {
result = IsRingConnected(is_connected);
}
if (result == Common::Input::DriverResult::Success && is_connected) {
LOG_INFO(Input, "Ringcon detected");
result = ConfigureRing();
}
if (result == Common::Input::DriverResult::Success) {
is_enabled = true;
}
return result;
}
Common::Input::DriverResult RingConProtocol::IsRingConnected(bool& is_connected) {
LOG_DEBUG(Input, "IsRingConnected");
constexpr std::size_t max_tries = 42;
SubCommandResponse output{};
std::size_t tries = 0;
is_connected = false;
do {
const auto result = SendSubCommand(SubCommand::GET_EXTERNAL_DEVICE_INFO, {}, output);
if (result != Common::Input::DriverResult::Success &&
result != Common::Input::DriverResult::Timeout) {
return result;
}
if (tries++ >= max_tries) {
return Common::Input::DriverResult::NoDeviceDetected;
}
} while (output.external_device_id != ExternalDeviceId::RingController);
is_connected = true;
return Common::Input::DriverResult::Success;
}
Common::Input::DriverResult RingConProtocol::ConfigureRing() {
LOG_DEBUG(Input, "ConfigureRing");
static constexpr std::array<u8, 37> ring_config{
0x06, 0x03, 0x25, 0x06, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x16, 0xED, 0x34, 0x36,
0x00, 0x00, 0x00, 0x0A, 0x64, 0x0B, 0xE6, 0xA9, 0x22, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xA8, 0xE1, 0x34, 0x36};
const Common::Input::DriverResult result =
SendSubCommand(SubCommand::SET_EXTERNAL_FORMAT_CONFIG, ring_config);
if (result != Common::Input::DriverResult::Success) {
return result;
}
static constexpr std::array<u8, 4> ringcon_data{0x04, 0x01, 0x01, 0x02};
return SendSubCommand(SubCommand::ENABLE_EXTERNAL_POLLING, ringcon_data);
}
bool RingConProtocol::IsEnabled() const {
return is_enabled;
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace Common::Input {
enum class DriverResult;
}
namespace InputCommon::Joycon {
class RingConProtocol final : private JoyconCommonProtocol {
public:
explicit RingConProtocol(std::shared_ptr<JoyconHandle> handle);
Common::Input::DriverResult EnableRingCon();
Common::Input::DriverResult DisableRingCon();
Common::Input::DriverResult StartRingconPolling();
bool IsEnabled() const;
private:
Common::Input::DriverResult IsRingConnected(bool& is_connected);
Common::Input::DriverResult ConfigureRing();
bool is_enabled{};
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,300 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cmath>
#include "common/input.h"
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/rumble.h"
namespace InputCommon::Joycon {
RumbleProtocol::RumbleProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
Common::Input::DriverResult RumbleProtocol::EnableRumble(bool is_enabled) {
LOG_DEBUG(Input, "Enable Rumble");
ScopedSetBlocking sb(this);
const std::array<u8, 1> buffer{static_cast<u8>(is_enabled ? 1 : 0)};
return SendSubCommand(SubCommand::ENABLE_VIBRATION, buffer);
}
Common::Input::DriverResult RumbleProtocol::SendVibration(const VibrationValue& vibration) {
std::array<u8, sizeof(DefaultVibrationBuffer)> buffer{};
if (vibration.high_amplitude <= 0.0f && vibration.low_amplitude <= 0.0f) {
return SendVibrationReport(DefaultVibrationBuffer);
}
// Protect joycons from damage from strong vibrations
const f32 clamp_amplitude =
1.0f / std::max(1.0f, vibration.high_amplitude + vibration.low_amplitude);
const u16 encoded_high_frequency = EncodeHighFrequency(vibration.high_frequency);
const u8 encoded_high_amplitude =
EncodeHighAmplitude(vibration.high_amplitude * clamp_amplitude);
const u8 encoded_low_frequency = EncodeLowFrequency(vibration.low_frequency);
const u16 encoded_low_amplitude = EncodeLowAmplitude(vibration.low_amplitude * clamp_amplitude);
buffer[0] = static_cast<u8>(encoded_high_frequency & 0xFF);
buffer[1] = static_cast<u8>(encoded_high_amplitude | ((encoded_high_frequency >> 8) & 0x01));
buffer[2] = static_cast<u8>(encoded_low_frequency | ((encoded_low_amplitude >> 8) & 0x80));
buffer[3] = static_cast<u8>(encoded_low_amplitude & 0xFF);
// Duplicate rumble for now
buffer[4] = buffer[0];
buffer[5] = buffer[1];
buffer[6] = buffer[2];
buffer[7] = buffer[3];
return SendVibrationReport(buffer);
}
u16 RumbleProtocol::EncodeHighFrequency(f32 frequency) const {
const u8 new_frequency =
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f));
return static_cast<u16>((new_frequency - 0x60) * 4);
}
u8 RumbleProtocol::EncodeLowFrequency(f32 frequency) const {
const u8 new_frequency =
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f));
return static_cast<u8>(new_frequency - 0x40);
}
u8 RumbleProtocol::EncodeHighAmplitude(f32 amplitude) const {
// More information about these values can be found here:
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
static constexpr std::array<std::pair<f32, int>, 101> high_frequency_amplitude{
std::pair<f32, int>{0.0f, 0x0},
{0.01f, 0x2},
{0.012f, 0x4},
{0.014f, 0x6},
{0.017f, 0x8},
{0.02f, 0x0a},
{0.024f, 0x0c},
{0.028f, 0x0e},
{0.033f, 0x10},
{0.04f, 0x12},
{0.047f, 0x14},
{0.056f, 0x16},
{0.067f, 0x18},
{0.08f, 0x1a},
{0.095f, 0x1c},
{0.112f, 0x1e},
{0.117f, 0x20},
{0.123f, 0x22},
{0.128f, 0x24},
{0.134f, 0x26},
{0.14f, 0x28},
{0.146f, 0x2a},
{0.152f, 0x2c},
{0.159f, 0x2e},
{0.166f, 0x30},
{0.173f, 0x32},
{0.181f, 0x34},
{0.189f, 0x36},
{0.198f, 0x38},
{0.206f, 0x3a},
{0.215f, 0x3c},
{0.225f, 0x3e},
{0.23f, 0x40},
{0.235f, 0x42},
{0.24f, 0x44},
{0.245f, 0x46},
{0.251f, 0x48},
{0.256f, 0x4a},
{0.262f, 0x4c},
{0.268f, 0x4e},
{0.273f, 0x50},
{0.279f, 0x52},
{0.286f, 0x54},
{0.292f, 0x56},
{0.298f, 0x58},
{0.305f, 0x5a},
{0.311f, 0x5c},
{0.318f, 0x5e},
{0.325f, 0x60},
{0.332f, 0x62},
{0.34f, 0x64},
{0.347f, 0x66},
{0.355f, 0x68},
{0.362f, 0x6a},
{0.37f, 0x6c},
{0.378f, 0x6e},
{0.387f, 0x70},
{0.395f, 0x72},
{0.404f, 0x74},
{0.413f, 0x76},
{0.422f, 0x78},
{0.431f, 0x7a},
{0.44f, 0x7c},
{0.45f, 0x7e},
{0.46f, 0x80},
{0.47f, 0x82},
{0.48f, 0x84},
{0.491f, 0x86},
{0.501f, 0x88},
{0.512f, 0x8a},
{0.524f, 0x8c},
{0.535f, 0x8e},
{0.547f, 0x90},
{0.559f, 0x92},
{0.571f, 0x94},
{0.584f, 0x96},
{0.596f, 0x98},
{0.609f, 0x9a},
{0.623f, 0x9c},
{0.636f, 0x9e},
{0.65f, 0xa0},
{0.665f, 0xa2},
{0.679f, 0xa4},
{0.694f, 0xa6},
{0.709f, 0xa8},
{0.725f, 0xaa},
{0.741f, 0xac},
{0.757f, 0xae},
{0.773f, 0xb0},
{0.79f, 0xb2},
{0.808f, 0xb4},
{0.825f, 0xb6},
{0.843f, 0xb8},
{0.862f, 0xba},
{0.881f, 0xbc},
{0.9f, 0xbe},
{0.92f, 0xc0},
{0.94f, 0xc2},
{0.96f, 0xc4},
{0.981f, 0xc6},
{1.003f, 0xc8},
};
for (const auto& [amplitude_value, code] : high_frequency_amplitude) {
if (amplitude <= amplitude_value) {
return static_cast<u8>(code);
}
}
return static_cast<u8>(high_frequency_amplitude[high_frequency_amplitude.size() - 1].second);
}
u16 RumbleProtocol::EncodeLowAmplitude(f32 amplitude) const {
// More information about these values can be found here:
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
static constexpr std::array<std::pair<f32, int>, 101> high_frequency_amplitude{
std::pair<f32, int>{0.0f, 0x0040},
{0.01f, 0x8040},
{0.012f, 0x0041},
{0.014f, 0x8041},
{0.017f, 0x0042},
{0.02f, 0x8042},
{0.024f, 0x0043},
{0.028f, 0x8043},
{0.033f, 0x0044},
{0.04f, 0x8044},
{0.047f, 0x0045},
{0.056f, 0x8045},
{0.067f, 0x0046},
{0.08f, 0x8046},
{0.095f, 0x0047},
{0.112f, 0x8047},
{0.117f, 0x0048},
{0.123f, 0x8048},
{0.128f, 0x0049},
{0.134f, 0x8049},
{0.14f, 0x004a},
{0.146f, 0x804a},
{0.152f, 0x004b},
{0.159f, 0x804b},
{0.166f, 0x004c},
{0.173f, 0x804c},
{0.181f, 0x004d},
{0.189f, 0x804d},
{0.198f, 0x004e},
{0.206f, 0x804e},
{0.215f, 0x004f},
{0.225f, 0x804f},
{0.23f, 0x0050},
{0.235f, 0x8050},
{0.24f, 0x0051},
{0.245f, 0x8051},
{0.251f, 0x0052},
{0.256f, 0x8052},
{0.262f, 0x0053},
{0.268f, 0x8053},
{0.273f, 0x0054},
{0.279f, 0x8054},
{0.286f, 0x0055},
{0.292f, 0x8055},
{0.298f, 0x0056},
{0.305f, 0x8056},
{0.311f, 0x0057},
{0.318f, 0x8057},
{0.325f, 0x0058},
{0.332f, 0x8058},
{0.34f, 0x0059},
{0.347f, 0x8059},
{0.355f, 0x005a},
{0.362f, 0x805a},
{0.37f, 0x005b},
{0.378f, 0x805b},
{0.387f, 0x005c},
{0.395f, 0x805c},
{0.404f, 0x005d},
{0.413f, 0x805d},
{0.422f, 0x005e},
{0.431f, 0x805e},
{0.44f, 0x005f},
{0.45f, 0x805f},
{0.46f, 0x0060},
{0.47f, 0x8060},
{0.48f, 0x0061},
{0.491f, 0x8061},
{0.501f, 0x0062},
{0.512f, 0x8062},
{0.524f, 0x0063},
{0.535f, 0x8063},
{0.547f, 0x0064},
{0.559f, 0x8064},
{0.571f, 0x0065},
{0.584f, 0x8065},
{0.596f, 0x0066},
{0.609f, 0x8066},
{0.623f, 0x0067},
{0.636f, 0x8067},
{0.65f, 0x0068},
{0.665f, 0x8068},
{0.679f, 0x0069},
{0.694f, 0x8069},
{0.709f, 0x006a},
{0.725f, 0x806a},
{0.741f, 0x006b},
{0.757f, 0x806b},
{0.773f, 0x006c},
{0.79f, 0x806c},
{0.808f, 0x006d},
{0.825f, 0x806d},
{0.843f, 0x006e},
{0.862f, 0x806e},
{0.881f, 0x006f},
{0.9f, 0x806f},
{0.92f, 0x0070},
{0.94f, 0x8070},
{0.96f, 0x0071},
{0.981f, 0x8071},
{1.003f, 0x0072},
};
for (const auto& [amplitude_value, code] : high_frequency_amplitude) {
if (amplitude <= amplitude_value) {
return static_cast<u16>(code);
}
}
return static_cast<u16>(high_frequency_amplitude[high_frequency_amplitude.size() - 1].second);
}
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace Common::Input {
enum class DriverResult;
}
namespace InputCommon::Joycon {
class RumbleProtocol final : private JoyconCommonProtocol {
public:
explicit RumbleProtocol(std::shared_ptr<JoyconHandle> handle);
Common::Input::DriverResult EnableRumble(bool is_enabled);
Common::Input::DriverResult SendVibration(const VibrationValue& vibration);
private:
u16 EncodeHighFrequency(f32 frequency) const;
u8 EncodeLowFrequency(f32 frequency) const;
u8 EncodeHighAmplitude(f32 amplitude) const;
u16 EncodeLowAmplitude(f32 amplitude) const;
};
} // namespace InputCommon::Joycon

View File

@@ -0,0 +1,332 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
#include <cmath>
#include "common/math_util.h"
#include "common/settings.h"
#include "input_common/helpers/stick_from_buttons.h"
namespace InputCommon {
class Stick final : public Common::Input::InputDevice {
public:
// Some games such as EARTH DEFENSE FORCE: WORLD BROTHERS
// do not play nicely with the theoretical maximum range.
// Using a value one lower from the maximum emulates real stick behavior.
static constexpr float MAX_RANGE = 32766.0f / 32767.0f;
static constexpr float TAU = Common::PI * 2.0f;
// Use wider angle to ease the transition.
static constexpr float APERTURE = TAU * 0.15f;
using Button = std::unique_ptr<Common::Input::InputDevice>;
Stick(Button up_, Button down_, Button left_, Button right_, Button modifier_, Button updater_,
float modifier_scale_, float modifier_angle_)
: up(std::move(up_)), down(std::move(down_)), left(std::move(left_)),
right(std::move(right_)), modifier(std::move(modifier_)), updater(std::move(updater_)),
modifier_scale(modifier_scale_), modifier_angle(modifier_angle_) {
up->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateUpButtonStatus(callback_);
},
});
down->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateDownButtonStatus(callback_);
},
});
left->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateLeftButtonStatus(callback_);
},
});
right->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateRightButtonStatus(callback_);
},
});
modifier->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateModButtonStatus(callback_);
},
});
updater->SetCallback({
.on_change = [this](const Common::Input::CallbackStatus& callback_) { SoftUpdate(); },
});
last_x_axis_value = 0.0f;
last_y_axis_value = 0.0f;
}
bool IsAngleGreater(float old_angle, float new_angle) const {
const float top_limit = new_angle + APERTURE;
return (old_angle > new_angle && old_angle <= top_limit) ||
(old_angle + TAU > new_angle && old_angle + TAU <= top_limit);
}
bool IsAngleSmaller(float old_angle, float new_angle) const {
const float bottom_limit = new_angle - APERTURE;
return (old_angle >= bottom_limit && old_angle < new_angle) ||
(old_angle - TAU >= bottom_limit && old_angle - TAU < new_angle);
}
float GetAngle(std::chrono::time_point<std::chrono::steady_clock> now) const {
float new_angle = angle;
auto time_difference = static_cast<float>(
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update).count());
time_difference /= 1000.0f;
if (time_difference > 0.5f) {
time_difference = 0.5f;
}
if (IsAngleGreater(new_angle, goal_angle)) {
new_angle -= modifier_angle * time_difference;
if (new_angle < 0) {
new_angle += TAU;
}
if (!IsAngleGreater(new_angle, goal_angle)) {
return goal_angle;
}
} else if (IsAngleSmaller(new_angle, goal_angle)) {
new_angle += modifier_angle * time_difference;
if (new_angle >= TAU) {
new_angle -= TAU;
}
if (!IsAngleSmaller(new_angle, goal_angle)) {
return goal_angle;
}
} else {
return goal_angle;
}
return new_angle;
}
void SetGoalAngle(bool r, bool l, bool u, bool d) {
// Move to the right
if (r && !u && !d) {
goal_angle = 0.0f;
}
// Move to the upper right
if (r && u && !d) {
goal_angle = Common::PI * 0.25f;
}
// Move up
if (u && !l && !r) {
goal_angle = Common::PI * 0.5f;
}
// Move to the upper left
if (l && u && !d) {
goal_angle = Common::PI * 0.75f;
}
// Move to the left
if (l && !u && !d) {
goal_angle = Common::PI;
}
// Move to the bottom left
if (l && !u && d) {
goal_angle = Common::PI * 1.25f;
}
// Move down
if (d && !l && !r) {
goal_angle = Common::PI * 1.5f;
}
// Move to the bottom right
if (r && !u && d) {
goal_angle = Common::PI * 1.75f;
}
}
void UpdateUpButtonStatus(const Common::Input::CallbackStatus& button_callback) {
up_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateDownButtonStatus(const Common::Input::CallbackStatus& button_callback) {
down_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateLeftButtonStatus(const Common::Input::CallbackStatus& button_callback) {
left_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateRightButtonStatus(const Common::Input::CallbackStatus& button_callback) {
right_status = button_callback.button_status.value;
UpdateStatus();
}
void UpdateModButtonStatus(const Common::Input::CallbackStatus& button_callback) {
const auto& new_status = button_callback.button_status;
const bool new_button_value = new_status.inverted ? !new_status.value : new_status.value;
modifier_status.toggle = new_status.toggle;
// Update button status with current
if (!modifier_status.toggle) {
modifier_status.locked = false;
if (modifier_status.value != new_button_value) {
modifier_status.value = new_button_value;
}
} else {
// Toggle button and lock status
if (new_button_value && !modifier_status.locked) {
modifier_status.locked = true;
modifier_status.value = !modifier_status.value;
}
// Unlock button ready for next press
if (!new_button_value && modifier_status.locked) {
modifier_status.locked = false;
}
}
UpdateStatus();
}
void UpdateStatus() {
bool r = right_status;
bool l = left_status;
bool u = up_status;
bool d = down_status;
// Eliminate contradictory movements
if (r && l) {
r = false;
l = false;
}
if (u && d) {
u = false;
d = false;
}
// Move if a key is pressed
if (r || l || u || d) {
amplitude = modifier_status.value ? modifier_scale : MAX_RANGE;
} else {
amplitude = 0;
}
const auto now = std::chrono::steady_clock::now();
const auto time_difference = static_cast<u64>(
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update).count());
if (time_difference < 10) {
// Disable analog mode if inputs are too fast
SetGoalAngle(r, l, u, d);
angle = goal_angle;
} else {
angle = GetAngle(now);
SetGoalAngle(r, l, u, d);
}
last_update = now;
Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Stick,
.stick_status = GetStatus(),
};
last_x_axis_value = status.stick_status.x.raw_value;
last_y_axis_value = status.stick_status.y.raw_value;
TriggerOnChange(status);
}
void ForceUpdate() override {
up->ForceUpdate();
down->ForceUpdate();
left->ForceUpdate();
right->ForceUpdate();
modifier->ForceUpdate();
}
void SoftUpdate() {
Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Stick,
.stick_status = GetStatus(),
};
if (last_x_axis_value == status.stick_status.x.raw_value &&
last_y_axis_value == status.stick_status.y.raw_value) {
return;
}
last_x_axis_value = status.stick_status.x.raw_value;
last_y_axis_value = status.stick_status.y.raw_value;
TriggerOnChange(status);
}
Common::Input::StickStatus GetStatus() const {
Common::Input::StickStatus status{};
status.x.properties = properties;
status.y.properties = properties;
if (Settings::values.emulate_analog_keyboard) {
const auto now = std::chrono::steady_clock::now();
const float angle_ = GetAngle(now);
status.x.raw_value = std::cos(angle_) * amplitude;
status.y.raw_value = std::sin(angle_) * amplitude;
return status;
}
status.x.raw_value = std::cos(goal_angle) * amplitude;
status.y.raw_value = std::sin(goal_angle) * amplitude;
return status;
}
private:
static constexpr Common::Input::AnalogProperties properties{
.deadzone = 0.0f,
.range = 1.0f,
.threshold = 0.5f,
.offset = 0.0f,
.inverted = false,
.toggle = false,
};
Button up;
Button down;
Button left;
Button right;
Button modifier;
Button updater;
float modifier_scale{};
float modifier_angle{};
float angle{};
float goal_angle{};
float amplitude{};
bool up_status{};
bool down_status{};
bool left_status{};
bool right_status{};
float last_x_axis_value{};
float last_y_axis_value{};
Common::Input::ButtonStatus modifier_status{};
std::chrono::time_point<std::chrono::steady_clock> last_update;
};
std::unique_ptr<Common::Input::InputDevice> StickFromButton::Create(
const Common::ParamPackage& params) {
const std::string null_engine = Common::ParamPackage{{"engine", "null"}}.Serialize();
auto up = Common::Input::CreateInputDeviceFromString(params.Get("up", null_engine));
auto down = Common::Input::CreateInputDeviceFromString(params.Get("down", null_engine));
auto left = Common::Input::CreateInputDeviceFromString(params.Get("left", null_engine));
auto right = Common::Input::CreateInputDeviceFromString(params.Get("right", null_engine));
auto modifier = Common::Input::CreateInputDeviceFromString(params.Get("modifier", null_engine));
auto updater = Common::Input::CreateInputDeviceFromString("engine:updater,button:0");
auto modifier_scale = params.Get("modifier_scale", 0.5f);
auto modifier_angle = params.Get("modifier_angle", 5.5f);
return std::make_unique<Stick>(std::move(up), std::move(down), std::move(left),
std::move(right), std::move(modifier), std::move(updater),
modifier_scale, modifier_angle);
}
} // namespace InputCommon

View File

@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/input.h"
namespace InputCommon {
/**
* An analog device factory that takes direction button devices and combines them into a analog
* device.
*/
class StickFromButton final : public Common::Input::Factory<Common::Input::InputDevice> {
public:
/**
* Creates an analog device from direction button devices
* @param params contains parameters for creating the device:
* - "up": a serialized ParamPackage for creating a button device for up direction
* - "down": a serialized ParamPackage for creating a button device for down direction
* - "left": a serialized ParamPackage for creating a button device for left direction
* - "right": a serialized ParamPackage for creating a button device for right direction
* - "modifier": a serialized ParamPackage for creating a button device as the modifier
* - "modifier_scale": a float for the multiplier the modifier gives to the position
*/
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
};
} // namespace InputCommon

View File

@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2020 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include "common/settings.h"
#include "input_common/helpers/touch_from_buttons.h"
namespace InputCommon {
class TouchFromButtonDevice final : public Common::Input::InputDevice {
public:
using Button = std::unique_ptr<Common::Input::InputDevice>;
TouchFromButtonDevice(Button button_, float x_, float y_)
: button(std::move(button_)), x(x_), y(y_) {
last_button_value = false;
button->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
UpdateButtonStatus(callback_);
},
});
button->ForceUpdate();
}
void ForceUpdate() override {
button->ForceUpdate();
}
Common::Input::TouchStatus GetStatus(bool pressed) const {
const Common::Input::ButtonStatus button_status{
.value = pressed,
};
Common::Input::TouchStatus status{
.pressed = button_status,
.x = {},
.y = {},
};
status.x.properties = properties;
status.y.properties = properties;
if (!pressed) {
return status;
}
status.x.raw_value = x;
status.y.raw_value = y;
return status;
}
void UpdateButtonStatus(const Common::Input::CallbackStatus& button_callback) {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Touch,
.touch_status = GetStatus(button_callback.button_status.value),
};
if (last_button_value != button_callback.button_status.value) {
last_button_value = button_callback.button_status.value;
TriggerOnChange(status);
}
}
private:
static constexpr Common::Input::AnalogProperties properties{
.deadzone = 0.0f,
.range = 1.0f,
.threshold = 0.5f,
.offset = 0.0f,
.inverted = false,
.toggle = false,
};
Button button;
bool last_button_value;
const float x;
const float y;
};
std::unique_ptr<Common::Input::InputDevice> TouchFromButton::Create(
const Common::ParamPackage& params) {
const std::string null_engine = Common::ParamPackage{{"engine", "null"}}.Serialize();
auto button = Common::Input::CreateInputDeviceFromString(params.Get("button", null_engine));
const float x = params.Get("x", 0.0f) / 1280.0f;
const float y = params.Get("y", 0.0f) / 720.0f;
return std::make_unique<TouchFromButtonDevice>(std::move(button), x, y);
}
} // namespace InputCommon

View File

@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2020 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/input.h"
namespace InputCommon {
/**
* A touch device factory that takes a list of button devices and combines them into a touch device.
*/
class TouchFromButton final : public Common::Input::Factory<Common::Input::InputDevice> {
public:
/**
* Creates a touch device from a list of button devices
*/
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
};
} // namespace InputCommon

View File

@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstddef>
#include <cstring>
#include "common/logging/log.h"
#include "input_common/helpers/udp_protocol.h"
namespace InputCommon::CemuhookUDP {
static constexpr std::size_t GetSizeOfResponseType(Type t) {
switch (t) {
case Type::Version:
return sizeof(Response::Version);
case Type::PortInfo:
return sizeof(Response::PortInfo);
case Type::PadData:
return sizeof(Response::PadData);
}
return 0;
}
namespace Response {
/**
* Returns Type if the packet is valid, else none
*
* Note: Modifies the buffer to zero out the crc (since that's the easiest way to check without
* copying the buffer)
*/
std::optional<Type> Validate(u8* data, std::size_t size) {
if (size < sizeof(Header)) {
return std::nullopt;
}
Header header{};
std::memcpy(&header, data, sizeof(Header));
if (header.magic != SERVER_MAGIC) {
LOG_ERROR(Input, "UDP Packet has an unexpected magic value");
return std::nullopt;
}
if (header.protocol_version != PROTOCOL_VERSION) {
LOG_ERROR(Input, "UDP Packet protocol mismatch");
return std::nullopt;
}
if (header.type < Type::Version || header.type > Type::PadData) {
LOG_ERROR(Input, "UDP Packet is an unknown type");
return std::nullopt;
}
// Packet size must equal sizeof(Header) + sizeof(Data)
// and also verify that the packet info mentions the correct size. Since the spec includes the
// type of the packet as part of the data, we need to include it in size calculations here
// ie: payload_length == sizeof(T) + sizeof(Type)
const std::size_t data_len = GetSizeOfResponseType(header.type);
if (header.payload_length != data_len + sizeof(Type) || size < data_len + sizeof(Header)) {
LOG_ERROR(
Input,
"UDP Packet payload length doesn't match. Received: {} PayloadLength: {} Expected: {}",
size, header.payload_length, data_len + sizeof(Type));
return std::nullopt;
}
const u32 crc32 = header.crc;
boost::crc_32_type result;
// zero out the crc in the buffer and then run the crc against it
std::memset(&data[offsetof(Header, crc)], 0, sizeof(u32_le));
result.process_bytes(data, data_len + sizeof(Header));
if (crc32 != result.checksum()) {
LOG_ERROR(Input, "UDP Packet CRC check failed. Offset: {}", offsetof(Header, crc));
return std::nullopt;
}
return header.type;
}
} // namespace Response
} // namespace InputCommon::CemuhookUDP

View File

@@ -0,0 +1,302 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <optional>
#include <type_traits>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4701) // Potentially uninitialized local variable 'result' used
#endif
#include <boost/crc.hpp>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include "common/swap.h"
namespace InputCommon::CemuhookUDP {
constexpr std::size_t MAX_PACKET_SIZE = 100;
constexpr u16 PROTOCOL_VERSION = 1001;
constexpr u32 CLIENT_MAGIC = 0x43555344; // DSUC (but flipped for LE)
constexpr u32 SERVER_MAGIC = 0x53555344; // DSUS (but flipped for LE)
enum class Type : u32 {
Version = 0x00100000,
PortInfo = 0x00100001,
PadData = 0x00100002,
};
struct Header {
u32_le magic{};
u16_le protocol_version{};
u16_le payload_length{};
u32_le crc{};
u32_le id{};
///> In the protocol, the type of the packet is not part of the header, but its convenient to
///> include in the header so the callee doesn't have to duplicate the type twice when building
///> the data
Type type{};
};
static_assert(sizeof(Header) == 20, "UDP Message Header struct has wrong size");
static_assert(std::is_trivially_copyable_v<Header>, "UDP Message Header is not trivially copyable");
using MacAddress = std::array<u8, 6>;
constexpr MacAddress EMPTY_MAC_ADDRESS = {0, 0, 0, 0, 0, 0};
#pragma pack(push, 1)
template <typename T>
struct Message {
Header header{};
T data;
};
#pragma pack(pop)
template <typename T>
constexpr Type GetMessageType();
template <typename T>
Message<T> CreateMessage(const u32 magic, const T data, const u32 sender_id) {
boost::crc_32_type crc;
Header header{
magic, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, sender_id, GetMessageType<T>(),
};
Message<T> message{header, data};
crc.process_bytes(&message, sizeof(Message<T>));
message.header.crc = crc.checksum();
return message;
}
namespace Request {
enum RegisterFlags : u8 {
AllPads,
PadID,
PadMACAddress,
};
struct Version {};
/**
* Requests the server to send information about what controllers are plugged into the ports
* In citron's case, we only have one controller, so for simplicity's sake, we can just send a
* request explicitly for the first controller port and leave it at that. In the future it would be
* nice to make this configurable
*/
constexpr u32 MAX_PORTS = 4;
struct PortInfo {
u32_le pad_count{}; ///> Number of ports to request data for
std::array<u8, MAX_PORTS> port;
};
static_assert(std::is_trivially_copyable_v<PortInfo>,
"UDP Request PortInfo is not trivially copyable");
/**
* Request the latest pad information from the server. If the server hasn't received this message
* from the client in a reasonable time frame, the server will stop sending updates. The default
* timeout seems to be 5 seconds.
*/
struct PadData {
/// Determines which method will be used as a look up for the controller
RegisterFlags flags{};
/// Index of the port of the controller to retrieve data about
u8 port_id{};
/// Mac address of the controller to retrieve data about
MacAddress mac;
};
static_assert(sizeof(PadData) == 8, "UDP Request PadData struct has wrong size");
static_assert(std::is_trivially_copyable_v<PadData>,
"UDP Request PadData is not trivially copyable");
/**
* Creates a message with the proper header data that can be sent to the server.
* @param data Request body to send
* @param client_id ID of the udp client (usually not checked on the server)
*/
template <typename T>
Message<T> Create(const T data, const u32 client_id = 0) {
return CreateMessage(CLIENT_MAGIC, data, client_id);
}
} // namespace Request
namespace Response {
enum class ConnectionType : u8 {
None,
Usb,
Bluetooth,
};
enum class State : u8 {
Disconnected,
Reserved,
Connected,
};
enum class Model : u8 {
None,
PartialGyro,
FullGyro,
Generic,
};
enum class Battery : u8 {
None = 0x00,
Dying = 0x01,
Low = 0x02,
Medium = 0x03,
High = 0x04,
Full = 0x05,
Charging = 0xEE,
Charged = 0xEF,
};
struct Version {
u16_le version{};
};
static_assert(sizeof(Version) == 2, "UDP Response Version struct has wrong size");
static_assert(std::is_trivially_copyable_v<Version>,
"UDP Response Version is not trivially copyable");
struct PortInfo {
u8 id{};
State state{};
Model model{};
ConnectionType connection_type{};
MacAddress mac;
Battery battery{};
u8 is_pad_active{};
};
static_assert(sizeof(PortInfo) == 12, "UDP Response PortInfo struct has wrong size");
static_assert(std::is_trivially_copyable_v<PortInfo>,
"UDP Response PortInfo is not trivially copyable");
struct TouchPad {
u8 is_active{};
u8 id{};
u16_le x{};
u16_le y{};
};
static_assert(sizeof(TouchPad) == 6, "UDP Response TouchPad struct has wrong size ");
#pragma pack(push, 1)
struct PadData {
PortInfo info{};
u32_le packet_counter{};
u16_le digital_button{};
// The following union isn't trivially copyable but we don't use this input anyway.
// union DigitalButton {
// u16_le button;
// BitField<0, 1, u16> button_1; // Share
// BitField<1, 1, u16> button_2; // L3
// BitField<2, 1, u16> button_3; // R3
// BitField<3, 1, u16> button_4; // Options
// BitField<4, 1, u16> button_5; // Up
// BitField<5, 1, u16> button_6; // Right
// BitField<6, 1, u16> button_7; // Down
// BitField<7, 1, u16> button_8; // Left
// BitField<8, 1, u16> button_9; // L2
// BitField<9, 1, u16> button_10; // R2
// BitField<10, 1, u16> button_11; // L1
// BitField<11, 1, u16> button_12; // R1
// BitField<12, 1, u16> button_13; // Triangle
// BitField<13, 1, u16> button_14; // Circle
// BitField<14, 1, u16> button_15; // Cross
// BitField<15, 1, u16> button_16; // Square
// } digital_button;
u8 home;
/// If the device supports a "click" on the touchpad, this will change to 1 when a click happens
u8 touch_hard_press{};
u8 left_stick_x{};
u8 left_stick_y{};
u8 right_stick_x{};
u8 right_stick_y{};
struct AnalogButton {
u8 button_dpad_left_analog{};
u8 button_dpad_down_analog{};
u8 button_dpad_right_analog{};
u8 button_dpad_up_analog{};
u8 button_square_analog{};
u8 button_cross_analog{};
u8 button_circle_analog{};
u8 button_triangle_analog{};
u8 button_r1_analog{};
u8 button_l1_analog{};
u8 trigger_r2{};
u8 trigger_l2{};
} analog_button;
std::array<TouchPad, 2> touch;
u64_le motion_timestamp;
struct Accelerometer {
float x{};
float y{};
float z{};
} accel;
struct Gyroscope {
float pitch{};
float yaw{};
float roll{};
} gyro;
};
#pragma pack(pop)
static_assert(sizeof(PadData) == 80, "UDP Response PadData struct has wrong size ");
static_assert(std::is_trivially_copyable_v<PadData>,
"UDP Response PadData is not trivially copyable");
static_assert(sizeof(Message<PadData>) == MAX_PACKET_SIZE,
"UDP MAX_PACKET_SIZE is no longer larger than Message<PadData>");
static_assert(sizeof(PadData::AnalogButton) == 12,
"UDP Response AnalogButton struct has wrong size ");
static_assert(sizeof(PadData::Accelerometer) == 12,
"UDP Response Accelerometer struct has wrong size ");
static_assert(sizeof(PadData::Gyroscope) == 12, "UDP Response Gyroscope struct has wrong size ");
/**
* Create a Response Message from the data
* @param data array of bytes sent from the server
* @return boost::none if it failed to parse or Type if it succeeded. The client can then safely
* copy the data into the appropriate struct for that Type
*/
std::optional<Type> Validate(u8* data, std::size_t size);
} // namespace Response
template <>
constexpr Type GetMessageType<Request::Version>() {
return Type::Version;
}
template <>
constexpr Type GetMessageType<Request::PortInfo>() {
return Type::PortInfo;
}
template <>
constexpr Type GetMessageType<Request::PadData>() {
return Type::PadData;
}
template <>
constexpr Type GetMessageType<Response::Version>() {
return Type::Version;
}
template <>
constexpr Type GetMessageType<Response::PortInfo>() {
return Type::PortInfo;
}
template <>
constexpr Type GetMessageType<Response::PadData>() {
return Type::PadData;
}
} // namespace InputCommon::CemuhookUDP

View File

@@ -0,0 +1,483 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/input_engine.h"
namespace InputCommon {
void InputEngine::PreSetController(const PadIdentifier& identifier) {
std::scoped_lock lock{mutex};
controller_list.try_emplace(identifier);
}
void InputEngine::PreSetButton(const PadIdentifier& identifier, int button) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.buttons.try_emplace(button, false);
}
void InputEngine::PreSetHatButton(const PadIdentifier& identifier, int button) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.hat_buttons.try_emplace(button, u8{0});
}
void InputEngine::PreSetAxis(const PadIdentifier& identifier, int axis) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.axes.try_emplace(axis, 0.0f);
}
void InputEngine::PreSetMotion(const PadIdentifier& identifier, int motion) {
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
controller.motions.try_emplace(motion);
}
void InputEngine::SetButton(const PadIdentifier& identifier, int button, bool value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.buttons.insert_or_assign(button, value);
}
}
TriggerOnButtonChange(identifier, button, value);
}
void InputEngine::SetHatButton(const PadIdentifier& identifier, int button, u8 value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.hat_buttons.insert_or_assign(button, value);
}
}
TriggerOnHatButtonChange(identifier, button, value);
}
void InputEngine::SetAxis(const PadIdentifier& identifier, int axis, f32 value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.axes.insert_or_assign(axis, value);
}
}
TriggerOnAxisChange(identifier, axis, value);
}
void InputEngine::SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.battery = value;
}
}
TriggerOnBatteryChange(identifier, value);
}
void InputEngine::SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.color = value;
}
}
TriggerOnColorChange(identifier, value);
}
void InputEngine::SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.motions.insert_or_assign(motion, value);
}
}
TriggerOnMotionChange(identifier, motion, value);
}
void InputEngine::SetCamera(const PadIdentifier& identifier,
const Common::Input::CameraStatus& value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.camera = value;
}
}
TriggerOnCameraChange(identifier, value);
}
void InputEngine::SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value) {
{
std::scoped_lock lock{mutex};
ControllerData& controller = controller_list.at(identifier);
if (!configuring) {
controller.nfc = value;
}
}
TriggerOnNfcChange(identifier, value);
}
bool InputEngine::GetButton(const PadIdentifier& identifier, int button) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return false;
}
const ControllerData& controller = controller_iter->second;
const auto button_iter = controller.buttons.find(button);
if (button_iter == controller.buttons.cend()) {
LOG_ERROR(Input, "Invalid button {}", button);
return false;
}
return button_iter->second;
}
bool InputEngine::GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return false;
}
const ControllerData& controller = controller_iter->second;
const auto hat_iter = controller.hat_buttons.find(button);
if (hat_iter == controller.hat_buttons.cend()) {
LOG_ERROR(Input, "Invalid hat button {}", button);
return false;
}
return (hat_iter->second & direction) != 0;
}
f32 InputEngine::GetAxis(const PadIdentifier& identifier, int axis) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return 0.0f;
}
const ControllerData& controller = controller_iter->second;
const auto axis_iter = controller.axes.find(axis);
if (axis_iter == controller.axes.cend()) {
LOG_ERROR(Input, "Invalid axis {}", axis);
return 0.0f;
}
return axis_iter->second;
}
Common::Input::BatteryLevel InputEngine::GetBattery(const PadIdentifier& identifier) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return Common::Input::BatteryLevel::Charging;
}
const ControllerData& controller = controller_iter->second;
return controller.battery;
}
Common::Input::BodyColorStatus InputEngine::GetColor(const PadIdentifier& identifier) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return {};
}
const ControllerData& controller = controller_iter->second;
return controller.color;
}
BasicMotion InputEngine::GetMotion(const PadIdentifier& identifier, int motion) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return {};
}
const ControllerData& controller = controller_iter->second;
return controller.motions.at(motion);
}
Common::Input::CameraStatus InputEngine::GetCamera(const PadIdentifier& identifier) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return {};
}
const ControllerData& controller = controller_iter->second;
return controller.camera;
}
Common::Input::NfcStatus InputEngine::GetNfc(const PadIdentifier& identifier) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
if (controller_iter == controller_list.cend()) {
LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
identifier.pad, identifier.port);
return {};
}
const ControllerData& controller = controller_iter->second;
return controller.nfc;
}
void InputEngine::ResetButtonState() {
for (const auto& controller : controller_list) {
for (const auto& button : controller.second.buttons) {
SetButton(controller.first, button.first, false);
}
for (const auto& button : controller.second.hat_buttons) {
SetHatButton(controller.first, button.first, 0);
}
}
}
void InputEngine::ResetAnalogState() {
for (const auto& controller : controller_list) {
for (const auto& axis : controller.second.axes) {
SetAxis(controller.first, axis.first, 0.0);
}
}
}
void InputEngine::TriggerOnButtonChange(const PadIdentifier& identifier, int button, bool value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Button, button)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
PreSetButton(identifier, button);
if (value == GetButton(identifier, button)) {
return;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::Button,
.index = button,
.button_value = value,
});
}
void InputEngine::TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::HatButton, button)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
for (std::size_t index = 1; index < 0xff; index <<= 1) {
bool button_value = (value & index) != 0;
if (button_value == GetHatButton(identifier, button, static_cast<u8>(index))) {
continue;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::HatButton,
.index = button,
.hat_name = GetHatButtonName(static_cast<u8>(index)),
});
}
}
void InputEngine::TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Analog, axis)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
if (std::abs(value - GetAxis(identifier, axis)) < 0.5f) {
return;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::Analog,
.index = axis,
.axis_value = value,
});
}
void InputEngine::TriggerOnBatteryChange(const PadIdentifier& identifier,
[[maybe_unused]] Common::Input::BatteryLevel value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Battery, 0)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
}
void InputEngine::TriggerOnColorChange(const PadIdentifier& identifier,
[[maybe_unused]] Common::Input::BodyColorStatus value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Color, 0)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
}
void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Motion, motion)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
if (!configuring || !mapping_callback.on_data) {
return;
}
const auto old_value = GetMotion(identifier, motion);
bool is_active = false;
if (std::abs(value.accel_x - old_value.accel_x) > 1.5f ||
std::abs(value.accel_y - old_value.accel_y) > 1.5f ||
std::abs(value.accel_z - old_value.accel_z) > 1.5f) {
is_active = true;
}
if (std::abs(value.gyro_x - old_value.gyro_x) > 0.6f ||
std::abs(value.gyro_y - old_value.gyro_y) > 0.6f ||
std::abs(value.gyro_z - old_value.gyro_z) > 0.6f) {
is_active = true;
}
if (!is_active) {
return;
}
mapping_callback.on_data(MappingData{
.engine = GetEngineName(),
.pad = identifier,
.type = EngineInputType::Motion,
.index = motion,
.motion_value = value,
});
}
void InputEngine::TriggerOnCameraChange(const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::CameraStatus& value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Camera, 0)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
}
void InputEngine::TriggerOnNfcChange(const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::NfcStatus& value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Nfc, 0)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
}
bool InputEngine::IsInputIdentifierEqual(const InputIdentifier& input_identifier,
const PadIdentifier& identifier, EngineInputType type,
int index) const {
if (input_identifier.type != type) {
return false;
}
if (input_identifier.index != index) {
return false;
}
if (input_identifier.identifier != identifier) {
return false;
}
return true;
}
void InputEngine::BeginConfiguration() {
configuring = true;
}
void InputEngine::EndConfiguration() {
configuring = false;
}
const std::string& InputEngine::GetEngineName() const {
return input_engine;
}
int InputEngine::SetCallback(InputIdentifier input_identifier) {
std::scoped_lock lock{mutex_callback};
callback_list.insert_or_assign(last_callback_key, std::move(input_identifier));
return last_callback_key++;
}
void InputEngine::SetMappingCallback(MappingCallback callback) {
std::scoped_lock lock{mutex_callback};
mapping_callback = std::move(callback);
}
void InputEngine::DeleteCallback(int key) {
std::scoped_lock lock{mutex_callback};
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 InputCommon

View File

@@ -0,0 +1,306 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <mutex>
#include <unordered_map>
#include "common/common_types.h"
#include "common/input.h"
#include "common/param_package.h"
#include "common/uuid.h"
#include "input_common/main.h"
// Pad Identifier of data source
struct PadIdentifier {
Common::UUID guid{};
std::size_t port{};
std::size_t pad{};
friend constexpr bool operator==(const PadIdentifier&, const PadIdentifier&) = default;
};
// Basic motion data containing data from the sensors and a timestamp in microseconds
struct BasicMotion {
float gyro_x{};
float gyro_y{};
float gyro_z{};
float accel_x{};
float accel_y{};
float accel_z{};
u64 delta_timestamp{};
};
// Types of input that are stored in the engine
enum class EngineInputType {
None,
Analog,
Battery,
Button,
Camera,
Color,
HatButton,
Motion,
Nfc,
};
struct VibrationRequest {
PadIdentifier identifier;
Common::Input::VibrationStatus vibration;
};
namespace std {
// Hash used to create lists from PadIdentifier data
template <>
struct hash<PadIdentifier> {
size_t operator()(const PadIdentifier& pad_id) const noexcept {
u64 hash_value = pad_id.guid.Hash();
hash_value ^= (static_cast<u64>(pad_id.port) << 32);
hash_value ^= static_cast<u64>(pad_id.pad);
return static_cast<size_t>(hash_value);
}
};
} // namespace std
namespace InputCommon {
// Data from the engine and device needed for creating a ParamPackage
struct MappingData {
std::string engine{};
PadIdentifier pad{};
EngineInputType type{};
int index{};
bool button_value{};
std::string hat_name{};
f32 axis_value{};
BasicMotion motion_value{};
};
// Triggered if data changed on the controller
struct UpdateCallback {
std::function<void()> on_change;
};
// Triggered if data changed on the controller and the engine is on configuring mode
struct MappingCallback {
std::function<void(const MappingData&)> on_data;
};
// Input Identifier of data source
struct InputIdentifier {
PadIdentifier identifier;
EngineInputType type;
int index;
UpdateCallback callback;
};
class InputEngine {
public:
explicit InputEngine(std::string input_engine_) : input_engine{std::move(input_engine_)} {}
virtual ~InputEngine() = default;
// Enable configuring mode for mapping
void BeginConfiguration();
// Disable configuring mode for mapping
void EndConfiguration();
// Sets a led pattern for a controller
virtual Common::Input::DriverResult SetLeds(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::LedStatus& led_status) {
return Common::Input::DriverResult::NotSupported;
}
// Sets rumble to a controller
virtual Common::Input::DriverResult SetVibration(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
return Common::Input::DriverResult::NotSupported;
}
// Returns true if device supports vibrations
virtual bool IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
return false;
}
// Sets polling mode to a controller
virtual Common::Input::DriverResult SetPollingMode(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::PollingMode polling_mode) {
return Common::Input::DriverResult::NotSupported;
}
// Sets camera format to a controller
virtual Common::Input::DriverResult SetCameraFormat(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] Common::Input::CameraFormat camera_format) {
return Common::Input::DriverResult::NotSupported;
}
// Returns success if nfc is supported
virtual Common::Input::NfcState SupportsNfc(
[[maybe_unused]] const PadIdentifier& identifier) const {
return Common::Input::NfcState::NotSupported;
}
// Start scanning for nfc tags
virtual Common::Input::NfcState StartNfcPolling(
[[maybe_unused]] const PadIdentifier& identifier_) {
return Common::Input::NfcState::NotSupported;
}
// Start scanning for nfc tags
virtual Common::Input::NfcState StopNfcPolling(
[[maybe_unused]] const PadIdentifier& identifier_) {
return Common::Input::NfcState::NotSupported;
}
// Reads data from amiibo tag
virtual Common::Input::NfcState ReadAmiiboData(
[[maybe_unused]] const PadIdentifier& identifier_,
[[maybe_unused]] std::vector<u8>& out_data) {
return Common::Input::NfcState::NotSupported;
}
// Writes data to an nfc tag
virtual Common::Input::NfcState WriteNfcData([[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const std::vector<u8>& data) {
return Common::Input::NfcState::NotSupported;
}
// Reads data from mifare tag
virtual Common::Input::NfcState ReadMifareData(
[[maybe_unused]] const PadIdentifier& identifier_,
[[maybe_unused]] const Common::Input::MifareRequest& request,
[[maybe_unused]] Common::Input::MifareRequest& out_data) {
return Common::Input::NfcState::NotSupported;
}
// Write data to mifare tag
virtual Common::Input::NfcState WriteMifareData(
[[maybe_unused]] const PadIdentifier& identifier_,
[[maybe_unused]] const Common::Input::MifareRequest& request) {
return Common::Input::NfcState::NotSupported;
}
// Returns the engine name
[[nodiscard]] const std::string& GetEngineName() const;
/// Used for automapping features
virtual std::vector<Common::ParamPackage> GetInputDevices() const {
return {};
}
/// Retrieves the button mappings for the given device
virtual ButtonMapping GetButtonMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
return {};
}
/// Retrieves the analog mappings for the given device
virtual AnalogMapping GetAnalogMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
return {};
}
/// Retrieves the motion mappings for the given device
virtual MotionMapping GetMotionMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
return {};
}
/// Retrieves the name of the given input.
virtual Common::Input::ButtonNames GetUIName(
[[maybe_unused]] const Common::ParamPackage& params) const {
return Common::Input::ButtonNames::Engine;
}
/// Retrieves the index number of the given hat button direction
virtual u8 GetHatButtonId([[maybe_unused]] const std::string& direction_name) const {
return 0;
}
/// Returns true if axis of a stick aren't mapped in the correct direction
virtual bool IsStickInverted([[maybe_unused]] const Common::ParamPackage& params) {
return false;
}
void PreSetController(const PadIdentifier& identifier);
void PreSetButton(const PadIdentifier& identifier, int button);
void PreSetHatButton(const PadIdentifier& identifier, int button);
void PreSetAxis(const PadIdentifier& identifier, int axis);
void PreSetMotion(const PadIdentifier& identifier, int motion);
void ResetButtonState();
void ResetAnalogState();
bool GetButton(const PadIdentifier& identifier, int button) const;
bool GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const;
f32 GetAxis(const PadIdentifier& identifier, int axis) const;
Common::Input::BatteryLevel GetBattery(const PadIdentifier& identifier) const;
Common::Input::BodyColorStatus GetColor(const PadIdentifier& identifier) const;
BasicMotion GetMotion(const PadIdentifier& identifier, int motion) const;
Common::Input::CameraStatus GetCamera(const PadIdentifier& identifier) const;
Common::Input::NfcStatus GetNfc(const PadIdentifier& identifier) const;
int SetCallback(InputIdentifier input_identifier);
void SetMappingCallback(MappingCallback callback);
void DeleteCallback(int key);
protected:
void SetButton(const PadIdentifier& identifier, int button, bool value);
void SetHatButton(const PadIdentifier& identifier, int button, u8 value);
void SetAxis(const PadIdentifier& identifier, int axis, f32 value);
void SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel value);
void SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value);
void SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value);
void SetCamera(const PadIdentifier& identifier, const Common::Input::CameraStatus& value);
void SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value);
virtual std::string GetHatButtonName([[maybe_unused]] u8 direction_value) const {
return "Unknown";
}
private:
struct ControllerData {
std::unordered_map<int, bool> buttons;
std::unordered_map<int, u8> hat_buttons;
std::unordered_map<int, float> axes;
std::unordered_map<int, BasicMotion> motions;
Common::Input::BatteryLevel battery{};
Common::Input::BodyColorStatus color{};
Common::Input::CameraStatus camera{};
Common::Input::NfcStatus nfc{};
};
void TriggerOnButtonChange(const PadIdentifier& identifier, int button, bool value);
void TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value);
void TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value);
void TriggerOnBatteryChange(const PadIdentifier& identifier, Common::Input::BatteryLevel value);
void TriggerOnColorChange(const PadIdentifier& identifier,
Common::Input::BodyColorStatus value);
void TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value);
void TriggerOnCameraChange(const PadIdentifier& identifier,
const Common::Input::CameraStatus& value);
void TriggerOnNfcChange(const PadIdentifier& identifier, const Common::Input::NfcStatus& value);
bool IsInputIdentifierEqual(const InputIdentifier& input_identifier,
const PadIdentifier& identifier, EngineInputType type,
int index) const;
mutable std::mutex mutex;
mutable std::mutex mutex_callback;
bool configuring{false};
const std::string input_engine;
int last_callback_key = 0;
std::unordered_map<PadIdentifier, ControllerData> controller_list;
std::unordered_map<int, InputIdentifier> callback_list;
MappingCallback mapping_callback;
};
} // namespace InputCommon

View File

@@ -0,0 +1,219 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/settings.h"
#include "input_common/input_engine.h"
#include "input_common/input_mapping.h"
namespace InputCommon {
MappingFactory::MappingFactory() = default;
void MappingFactory::BeginMapping(Polling::InputType type) {
is_enabled = true;
input_type = type;
input_queue.Clear();
first_axis = -1;
second_axis = -1;
}
Common::ParamPackage MappingFactory::GetNextInput() {
Common::ParamPackage input;
input_queue.Pop(input);
return input;
}
void MappingFactory::RegisterInput(const MappingData& data) {
if (!is_enabled) {
return;
}
if (!IsDriverValid(data)) {
return;
}
switch (input_type) {
case Polling::InputType::Button:
RegisterButton(data);
return;
case Polling::InputType::Stick:
RegisterStick(data);
return;
case Polling::InputType::Motion:
RegisterMotion(data);
return;
default:
return;
}
}
void MappingFactory::StopMapping() {
is_enabled = false;
input_type = Polling::InputType::None;
input_queue.Clear();
}
void MappingFactory::RegisterButton(const MappingData& data) {
Common::ParamPackage new_input;
new_input.Set("engine", data.engine);
if (data.pad.guid.IsValid()) {
new_input.Set("guid", data.pad.guid.RawString());
}
new_input.Set("port", static_cast<int>(data.pad.port));
new_input.Set("pad", static_cast<int>(data.pad.pad));
switch (data.type) {
case EngineInputType::Button:
// Workaround for old compatibility
if (data.engine == "keyboard") {
new_input.Set("code", data.index);
break;
}
new_input.Set("button", data.index);
break;
case EngineInputType::HatButton:
new_input.Set("hat", data.index);
new_input.Set("direction", data.hat_name);
break;
case EngineInputType::Analog:
// Ignore mouse axis when mapping buttons
if (data.engine == "mouse" && data.index != 4) {
return;
}
new_input.Set("axis", data.index);
new_input.Set("threshold", 0.5f);
break;
case EngineInputType::Motion:
new_input.Set("motion", data.index);
break;
default:
return;
}
input_queue.Push(new_input);
}
void MappingFactory::RegisterStick(const MappingData& data) {
Common::ParamPackage new_input;
new_input.Set("engine", data.engine);
if (data.pad.guid.IsValid()) {
new_input.Set("guid", data.pad.guid.RawString());
}
new_input.Set("port", static_cast<int>(data.pad.port));
new_input.Set("pad", static_cast<int>(data.pad.pad));
// If engine is mouse map the mouse position as a joystick
if (data.engine == "mouse") {
new_input.Set("axis_x", 0);
new_input.Set("axis_y", 1);
new_input.Set("threshold", 0.5f);
new_input.Set("range", 1.0f);
new_input.Set("deadzone", 0.0f);
input_queue.Push(new_input);
return;
}
switch (data.type) {
case EngineInputType::Button:
case EngineInputType::HatButton:
RegisterButton(data);
return;
case EngineInputType::Analog:
if (first_axis == data.index) {
return;
}
if (first_axis == -1) {
first_axis = data.index;
return;
}
new_input.Set("axis_x", first_axis);
new_input.Set("axis_y", data.index);
new_input.Set("threshold", 0.5f);
new_input.Set("range", 0.95f);
new_input.Set("deadzone", 0.15f);
break;
default:
return;
}
input_queue.Push(new_input);
}
void MappingFactory::RegisterMotion(const MappingData& data) {
Common::ParamPackage new_input;
new_input.Set("engine", data.engine);
if (data.pad.guid.IsValid()) {
new_input.Set("guid", data.pad.guid.RawString());
}
new_input.Set("port", static_cast<int>(data.pad.port));
new_input.Set("pad", static_cast<int>(data.pad.pad));
// If engine is mouse map it automatically to mouse motion
if (data.engine == "mouse") {
new_input.Set("motion", 0);
new_input.Set("pad", 1);
new_input.Set("threshold", 0.001f);
input_queue.Push(new_input);
return;
}
switch (data.type) {
case EngineInputType::Button:
case EngineInputType::HatButton:
RegisterButton(data);
return;
case EngineInputType::Analog:
if (first_axis == data.index) {
return;
}
if (second_axis == data.index) {
return;
}
if (first_axis == -1) {
first_axis = data.index;
return;
}
if (second_axis == -1) {
second_axis = data.index;
return;
}
new_input.Set("axis_x", first_axis);
new_input.Set("axis_y", second_axis);
new_input.Set("axis_z", data.index);
new_input.Set("range", 1.0f);
new_input.Set("deadzone", 0.20f);
break;
case EngineInputType::Motion:
new_input.Set("motion", data.index);
break;
default:
return;
}
input_queue.Push(new_input);
}
bool MappingFactory::IsDriverValid(const MappingData& data) const {
// Only port 0 can be mapped on the keyboard
if (data.engine == "keyboard" && data.pad.port != 0) {
return false;
}
// Only port 0 can be mapped on the mouse
if (data.engine == "mouse" && data.pad.port != 0) {
return false;
}
// To prevent mapping with two devices we disable any UDP except motion
if (!Settings::values.enable_udp_controller && data.engine == "cemuhookudp" &&
data.type != EngineInputType::Motion) {
return false;
}
// The following drivers don't need to be mapped
if (data.engine == "touch_from_button") {
return false;
}
if (data.engine == "analog_from_button") {
return false;
}
if (data.engine == "virtual_gamepad") {
return false;
}
return true;
}
} // namespace InputCommon

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/param_package.h"
#include "common/threadsafe_queue.h"
namespace InputCommon::Polling {
enum class InputType;
}
namespace InputCommon {
class InputEngine;
struct MappingData;
class MappingFactory {
public:
MappingFactory();
/**
* Resets all variables to begin the mapping process
* @param type type of input desired to be returned
*/
void BeginMapping(Polling::InputType type);
/// Returns an input event with mapping information from the input_queue
[[nodiscard]] Common::ParamPackage GetNextInput();
/**
* Registers mapping input data from the driver
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterInput(const MappingData& data);
/// Stop polling from all backends
void StopMapping();
private:
/**
* If provided data satisfies the requirements it will push an element to the input_queue
* Supported input:
* - Button: Creates a basic button ParamPackage
* - HatButton: Creates a basic hat button ParamPackage
* - Analog: Creates a basic analog ParamPackage
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterButton(const MappingData& data);
/**
* If provided data satisfies the requirements it will push an element to the input_queue
* Supported input:
* - Button, HatButton: Pass the data to RegisterButton
* - Analog: Stores the first axis and on the second axis creates a basic stick ParamPackage
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterStick(const MappingData& data);
/**
* If provided data satisfies the requirements it will push an element to the input_queue
* Supported input:
* - Button, HatButton: Pass the data to RegisterButton
* - Analog: Stores the first two axis and on the third axis creates a basic Motion
* ParamPackage
* - Motion: Creates a basic Motion ParamPackage
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
void RegisterMotion(const MappingData& data);
/**
* Returns true if driver can be mapped
* @param data A struct containing all the information needed to create a proper
* ParamPackage
*/
bool IsDriverValid(const MappingData& data) const;
Common::SPSCQueue<Common::ParamPackage> input_queue;
Polling::InputType input_type{Polling::InputType::None};
bool is_enabled{};
int first_axis = -1;
int second_axis = -1;
};
} // namespace InputCommon

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,248 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
namespace Input {
class InputDevice;
template <typename InputDevice>
class Factory;
}; // namespace Input
namespace InputCommon {
class InputEngine;
class OutputFactory final : public Common::Input::Factory<Common::Input::OutputDevice> {
public:
explicit OutputFactory(std::shared_ptr<InputEngine> input_engine_);
/**
* Creates an output device from the parameters given.
* @param params contains parameters for creating the device:
* - "guid" text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique output device with the parameters specified
*/
std::unique_ptr<Common::Input::OutputDevice> Create(
const Common::ParamPackage& params) override;
private:
std::shared_ptr<InputEngine> input_engine;
};
/**
* An Input factory. It receives input events and forward them to all input devices it created.
*/
class InputFactory final : public Common::Input::Factory<Common::Input::InputDevice> {
public:
explicit InputFactory(std::shared_ptr<InputEngine> input_engine_);
/**
* Creates an input device from the parameters given. Identifies the type of input to be
* returned if it contains the following parameters:
* - button: Contains "button" or "code"
* - hat_button: Contains "hat"
* - analog: Contains "axis"
* - trigger: Contains "button" and "axis"
* - stick: Contains "axis_x" and "axis_y"
* - motion: Contains "axis_x", "axis_y" and "axis_z"
* - motion: Contains "motion"
* - touch: Contains "button", "axis_x" and "axis_y"
* - battery: Contains "battery"
* - output: Contains "output"
* @param params contains parameters for creating the device:
* - "code": the code of the keyboard key to bind with the input
* - "button": same as "code" but for controller buttons
* - "hat": similar as "button" but it's a group of hat buttons from SDL
* - "axis": the axis number of the axis to bind with the input
* - "motion": the motion number of the motion to bind with the input
* - "axis_x": same as axis but specifying horizontal direction
* - "axis_y": same as axis but specifying vertical direction
* - "axis_z": same as axis but specifying forward direction
* - "battery": Only used as a placeholder to set the input type
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
private:
/**
* Creates a button device from the parameters given.
* @param params contains parameters for creating the device:
* - "code": the code of the keyboard key to bind with the input
* - "button": same as "code" but for controller buttons
* - "toggle": press once to enable, press again to disable
* - "inverted": inverts the output of the button
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateButtonDevice(
const Common::ParamPackage& params);
/**
* Creates a hat button device from the parameters given.
* @param params contains parameters for creating the device:
* - "button": the controller hat id to bind with the input
* - "direction": the direction id to be detected
* - "toggle": press once to enable, press again to disable
* - "inverted": inverts the output of the button
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateHatButtonDevice(
const Common::ParamPackage& params);
/**
* Creates a stick device from the parameters given.
* @param params contains parameters for creating the device:
* - "axis_x": the controller horizontal axis id to bind with the input
* - "axis_y": the controller vertical axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "threshold": the minimum required value to considered pressed
* - "offset_x": the amount of offset in the x axis
* - "offset_y": the amount of offset in the y axis
* - "invert_x": inverts the sign of the horizontal axis
* - "invert_y": inverts the sign of the vertical axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateStickDevice(
const Common::ParamPackage& params);
/**
* Creates an analog device from the parameters given.
* @param params contains parameters for creating the device:
* - "axis": the controller axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "threshold": the minimum required value to considered pressed
* - "offset": the amount of offset in the axis
* - "invert": inverts the sign of the axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateAnalogDevice(
const Common::ParamPackage& params);
/**
* Creates a trigger device from the parameters given.
* @param params contains parameters for creating the device:
* - "button": the controller hat id to bind with the input
* - "direction": the direction id to be detected
* - "toggle": press once to enable, press again to disable
* - "inverted": inverts the output of the button
* - "axis": the controller axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "threshold": the minimum required value to considered pressed
* - "offset": the amount of offset in the axis
* - "invert": inverts the sign of the axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateTriggerDevice(
const Common::ParamPackage& params);
/**
* Creates a touch device from the parameters given.
* @param params contains parameters for creating the device:
* - "button": the controller hat id to bind with the input
* - "direction": the direction id to be detected
* - "toggle": press once to enable, press again to disable
* - "inverted": inverts the output of the button
* - "axis_x": the controller horizontal axis id to bind with the input
* - "axis_y": the controller vertical axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "threshold": the minimum required value to considered pressed
* - "offset_x": the amount of offset in the x axis
* - "offset_y": the amount of offset in the y axis
* - "invert_x": inverts the sign of the horizontal axis
* - "invert_y": inverts the sign of the vertical axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateTouchDevice(
const Common::ParamPackage& params);
/**
* Creates a battery device from the parameters given.
* @param params contains parameters for creating the device:
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateBatteryDevice(
const Common::ParamPackage& params);
/**
* Creates a color device from the parameters given.
* @param params contains parameters for creating the device:
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateColorDevice(
const Common::ParamPackage& params);
/**
* Creates a motion device from the parameters given.
* @param params contains parameters for creating the device:
* - "axis_x": the controller horizontal axis id to bind with the input
* - "axis_y": the controller vertical axis id to bind with the input
* - "axis_z": the controller forward axis id to bind with the input
* - "deadzone": the minimum required value to be detected
* - "range": the maximum value required to reach 100%
* - "offset_x": the amount of offset in the x axis
* - "offset_y": the amount of offset in the y axis
* - "offset_z": the amount of offset in the z axis
* - "invert_x": inverts the sign of the horizontal axis
* - "invert_y": inverts the sign of the vertical axis
* - "invert_z": inverts the sign of the forward axis
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateMotionDevice(Common::ParamPackage params);
/**
* Creates a camera device from the parameters given.
* @param params contains parameters for creating the device:
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateCameraDevice(
const Common::ParamPackage& params);
/**
* Creates a nfc device from the parameters given.
* @param params contains parameters for creating the device:
* - "guid": text string for identifying controllers
* - "port": port of the connected device
* - "pad": slot of the connected controller
* @returns a unique input device with the parameters specified
*/
std::unique_ptr<Common::Input::InputDevice> CreateNfcDevice(const Common::ParamPackage& params);
std::shared_ptr<InputEngine> input_engine;
};
} // namespace InputCommon

512
src/input_common/main.cpp Normal file
View File

@@ -0,0 +1,512 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <memory>
#include "common/input.h"
#include "common/param_package.h"
#include "input_common/drivers/camera.h"
#include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h"
#include "input_common/drivers/tas_input.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "input_common/drivers/virtual_gamepad.h"
#include "input_common/helpers/stick_from_buttons.h"
#include "input_common/helpers/touch_from_buttons.h"
#include "input_common/input_engine.h"
#include "input_common/input_mapping.h"
#include "input_common/input_poller.h"
#include "input_common/main.h"
#ifdef HAVE_LIBUSB
#include "input_common/drivers/gc_adapter.h"
#endif
#ifdef HAVE_SDL2
#include "input_common/drivers/joycon.h"
#include "input_common/drivers/sdl_driver.h"
#endif
#ifdef ANDROID
#include "input_common/drivers/android.h"
#endif
namespace InputCommon {
/// Dummy engine to get periodic updates
class UpdateEngine final : public InputEngine {
public:
explicit UpdateEngine(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(identifier);
}
void PumpEvents() {
SetButton(identifier, 0, last_state);
last_state = !last_state;
}
private:
static constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
bool last_state{};
};
struct InputSubsystem::Impl {
template <typename Engine>
void RegisterEngine(std::string name, std::shared_ptr<Engine>& engine) {
MappingCallback mapping_callback{[this](const MappingData& data) { RegisterInput(data); }};
engine = std::make_shared<Engine>(name);
engine->SetMappingCallback(mapping_callback);
std::shared_ptr<InputFactory> input_factory = std::make_shared<InputFactory>(engine);
std::shared_ptr<OutputFactory> output_factory = std::make_shared<OutputFactory>(engine);
Common::Input::RegisterInputFactory(engine->GetEngineName(), std::move(input_factory));
Common::Input::RegisterOutputFactory(engine->GetEngineName(), std::move(output_factory));
}
void Initialize() {
mapping_factory = std::make_shared<MappingFactory>();
RegisterEngine("updater", update_engine);
RegisterEngine("keyboard", keyboard);
RegisterEngine("mouse", mouse);
RegisterEngine("touch", touch_screen);
#ifdef HAVE_LIBUSB
RegisterEngine("gcpad", gcadapter);
#endif
RegisterEngine("cemuhookudp", udp_client);
RegisterEngine("tas", tas_input);
RegisterEngine("camera", camera);
#ifdef ANDROID
RegisterEngine("android", android);
#endif
RegisterEngine("virtual_amiibo", virtual_amiibo);
RegisterEngine("virtual_gamepad", virtual_gamepad);
#ifdef HAVE_SDL2
RegisterEngine("sdl", sdl);
RegisterEngine("joycon", joycon);
#endif
Common::Input::RegisterInputFactory("touch_from_button",
std::make_shared<TouchFromButton>());
Common::Input::RegisterInputFactory("analog_from_button",
std::make_shared<StickFromButton>());
}
template <typename Engine>
void UnregisterEngine(std::shared_ptr<Engine>& engine) {
Common::Input::UnregisterInputFactory(engine->GetEngineName());
Common::Input::UnregisterOutputFactory(engine->GetEngineName());
engine.reset();
}
void Shutdown() {
UnregisterEngine(update_engine);
UnregisterEngine(keyboard);
UnregisterEngine(mouse);
UnregisterEngine(touch_screen);
#ifdef HAVE_LIBUSB
UnregisterEngine(gcadapter);
#endif
UnregisterEngine(udp_client);
UnregisterEngine(tas_input);
UnregisterEngine(camera);
#ifdef ANDROID
UnregisterEngine(android);
#endif
UnregisterEngine(virtual_amiibo);
UnregisterEngine(virtual_gamepad);
#ifdef HAVE_SDL2
UnregisterEngine(sdl);
UnregisterEngine(joycon);
#endif
Common::Input::UnregisterInputFactory("touch_from_button");
Common::Input::UnregisterInputFactory("analog_from_button");
}
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
std::vector<Common::ParamPackage> devices = {
Common::ParamPackage{{"display", "Any"}, {"engine", "any"}},
};
#ifndef ANDROID
auto keyboard_devices = keyboard->GetInputDevices();
devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end());
auto mouse_devices = mouse->GetInputDevices();
devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end());
#endif
#ifdef ANDROID
auto android_devices = android->GetInputDevices();
devices.insert(devices.end(), android_devices.begin(), android_devices.end());
#endif
#ifdef HAVE_LIBUSB
auto gcadapter_devices = gcadapter->GetInputDevices();
devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end());
#endif
auto udp_devices = udp_client->GetInputDevices();
devices.insert(devices.end(), udp_devices.begin(), udp_devices.end());
#ifdef HAVE_SDL2
auto joycon_devices = joycon->GetInputDevices();
devices.insert(devices.end(), joycon_devices.begin(), joycon_devices.end());
auto sdl_devices = sdl->GetInputDevices();
devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
#endif
return devices;
}
[[nodiscard]] std::shared_ptr<InputEngine> GetInputEngine(
const Common::ParamPackage& params) const {
if (!params.Has("engine") || params.Get("engine", "") == "any") {
return nullptr;
}
const std::string engine = params.Get("engine", "");
if (engine == keyboard->GetEngineName()) {
return keyboard;
}
if (engine == mouse->GetEngineName()) {
return mouse;
}
#ifdef ANDROID
if (engine == android->GetEngineName()) {
return android;
}
#endif
#ifdef HAVE_LIBUSB
if (engine == gcadapter->GetEngineName()) {
return gcadapter;
}
#endif
if (engine == udp_client->GetEngineName()) {
return udp_client;
}
#ifdef HAVE_SDL2
if (engine == sdl->GetEngineName()) {
return sdl;
}
if (engine == joycon->GetEngineName()) {
return joycon;
}
#endif
return nullptr;
}
[[nodiscard]] AnalogMapping GetAnalogMappingForDevice(
const Common::ParamPackage& params) const {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return {};
}
return input_engine->GetAnalogMappingForDevice(params);
}
[[nodiscard]] ButtonMapping GetButtonMappingForDevice(
const Common::ParamPackage& params) const {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return {};
}
return input_engine->GetButtonMappingForDevice(params);
}
[[nodiscard]] MotionMapping GetMotionMappingForDevice(
const Common::ParamPackage& params) const {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return {};
}
return input_engine->GetMotionMappingForDevice(params);
}
Common::Input::ButtonNames GetButtonName(const Common::ParamPackage& params) const {
if (!params.Has("engine") || params.Get("engine", "") == "any") {
return Common::Input::ButtonNames::Undefined;
}
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return Common::Input::ButtonNames::Invalid;
}
return input_engine->GetUIName(params);
}
bool IsStickInverted(const Common::ParamPackage& params) {
const auto input_engine = GetInputEngine(params);
if (input_engine == nullptr) {
return false;
}
return input_engine->IsStickInverted(params);
}
bool IsController(const Common::ParamPackage& params) {
const std::string engine = params.Get("engine", "");
if (engine == mouse->GetEngineName()) {
return true;
}
#ifdef ANDROID
if (engine == android->GetEngineName()) {
return true;
}
#endif
#ifdef HAVE_LIBUSB
if (engine == gcadapter->GetEngineName()) {
return true;
}
#endif
if (engine == udp_client->GetEngineName()) {
return true;
}
if (engine == tas_input->GetEngineName()) {
return true;
}
if (engine == virtual_gamepad->GetEngineName()) {
return true;
}
#ifdef HAVE_SDL2
if (engine == sdl->GetEngineName()) {
return true;
}
if (engine == joycon->GetEngineName()) {
return true;
}
#endif
return false;
}
void BeginConfiguration() {
keyboard->BeginConfiguration();
mouse->BeginConfiguration();
#ifdef ANDROID
android->BeginConfiguration();
#endif
#ifdef HAVE_LIBUSB
gcadapter->BeginConfiguration();
#endif
udp_client->BeginConfiguration();
#ifdef HAVE_SDL2
sdl->BeginConfiguration();
joycon->BeginConfiguration();
#endif
}
void EndConfiguration() {
keyboard->EndConfiguration();
mouse->EndConfiguration();
#ifdef ANDROID
android->EndConfiguration();
#endif
#ifdef HAVE_LIBUSB
gcadapter->EndConfiguration();
#endif
udp_client->EndConfiguration();
#ifdef HAVE_SDL2
sdl->EndConfiguration();
joycon->EndConfiguration();
#endif
}
void PumpEvents() const {
update_engine->PumpEvents();
#ifdef HAVE_SDL2
sdl->PumpEvents();
#endif
}
void RegisterInput(const MappingData& data) {
mapping_factory->RegisterInput(data);
}
std::shared_ptr<MappingFactory> mapping_factory;
std::shared_ptr<UpdateEngine> update_engine;
std::shared_ptr<Keyboard> keyboard;
std::shared_ptr<Mouse> mouse;
std::shared_ptr<TouchScreen> touch_screen;
std::shared_ptr<TasInput::Tas> tas_input;
std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
std::shared_ptr<Camera> camera;
std::shared_ptr<VirtualAmiibo> virtual_amiibo;
std::shared_ptr<VirtualGamepad> virtual_gamepad;
#ifdef HAVE_LIBUSB
std::shared_ptr<GCAdapter> gcadapter;
#endif
#ifdef HAVE_SDL2
std::shared_ptr<SDLDriver> sdl;
std::shared_ptr<Joycons> joycon;
#endif
#ifdef ANDROID
std::shared_ptr<Android> android;
#endif
};
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
InputSubsystem::~InputSubsystem() = default;
void InputSubsystem::Initialize() {
impl->Initialize();
}
void InputSubsystem::Shutdown() {
impl->Shutdown();
}
Keyboard* InputSubsystem::GetKeyboard() {
return impl->keyboard.get();
}
const Keyboard* InputSubsystem::GetKeyboard() const {
return impl->keyboard.get();
}
Mouse* InputSubsystem::GetMouse() {
return impl->mouse.get();
}
const Mouse* InputSubsystem::GetMouse() const {
return impl->mouse.get();
}
TouchScreen* InputSubsystem::GetTouchScreen() {
return impl->touch_screen.get();
}
const TouchScreen* InputSubsystem::GetTouchScreen() const {
return impl->touch_screen.get();
}
TasInput::Tas* InputSubsystem::GetTas() {
return impl->tas_input.get();
}
const TasInput::Tas* InputSubsystem::GetTas() const {
return impl->tas_input.get();
}
Camera* InputSubsystem::GetCamera() {
return impl->camera.get();
}
const Camera* InputSubsystem::GetCamera() const {
return impl->camera.get();
}
#ifdef ANDROID
Android* InputSubsystem::GetAndroid() {
return impl->android.get();
}
const Android* InputSubsystem::GetAndroid() const {
return impl->android.get();
}
#endif
VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() {
return impl->virtual_amiibo.get();
}
const VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() const {
return impl->virtual_amiibo.get();
}
VirtualGamepad* InputSubsystem::GetVirtualGamepad() {
return impl->virtual_gamepad.get();
}
const VirtualGamepad* InputSubsystem::GetVirtualGamepad() const {
return impl->virtual_gamepad.get();
}
std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
return impl->GetInputDevices();
}
AnalogMapping InputSubsystem::GetAnalogMappingForDevice(const Common::ParamPackage& device) const {
return impl->GetAnalogMappingForDevice(device);
}
ButtonMapping InputSubsystem::GetButtonMappingForDevice(const Common::ParamPackage& device) const {
return impl->GetButtonMappingForDevice(device);
}
MotionMapping InputSubsystem::GetMotionMappingForDevice(const Common::ParamPackage& device) const {
return impl->GetMotionMappingForDevice(device);
}
Common::Input::ButtonNames InputSubsystem::GetButtonName(const Common::ParamPackage& params) const {
return impl->GetButtonName(params);
}
bool InputSubsystem::IsController(const Common::ParamPackage& params) const {
return impl->IsController(params);
}
bool InputSubsystem::IsStickInverted(const Common::ParamPackage& params) const {
if (params.Has("axis_x") && params.Has("axis_y")) {
return impl->IsStickInverted(params);
}
return false;
}
void InputSubsystem::ReloadInputDevices() {
impl->udp_client.get()->ReloadSockets();
}
void InputSubsystem::BeginMapping(Polling::InputType type) {
impl->BeginConfiguration();
impl->mapping_factory->BeginMapping(type);
}
Common::ParamPackage InputSubsystem::GetNextInput() const {
return impl->mapping_factory->GetNextInput();
}
void InputSubsystem::StopMapping() const {
impl->EndConfiguration();
impl->mapping_factory->StopMapping();
}
void InputSubsystem::PumpEvents() const {
impl->PumpEvents();
}
std::string GenerateKeyboardParam(int key_code) {
Common::ParamPackage param;
param.Set("engine", "keyboard");
param.Set("code", key_code);
param.Set("toggle", false);
return param.Serialize();
}
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
int key_modifier, float modifier_scale) {
Common::ParamPackage circle_pad_param{
{"engine", "analog_from_button"},
{"up", GenerateKeyboardParam(key_up)},
{"down", GenerateKeyboardParam(key_down)},
{"left", GenerateKeyboardParam(key_left)},
{"right", GenerateKeyboardParam(key_right)},
{"modifier", GenerateKeyboardParam(key_modifier)},
{"modifier_scale", std::to_string(modifier_scale)},
};
return circle_pad_param.Serialize();
}
} // namespace InputCommon

178
src/input_common/main.h Normal file
View File

@@ -0,0 +1,178 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace Common {
class ParamPackage;
}
namespace Common::Input {
enum class ButtonNames;
}
namespace Settings::NativeAnalog {
enum Values : int;
}
namespace Settings::NativeButton {
enum Values : int;
}
namespace Settings::NativeMotion {
enum Values : int;
}
namespace InputCommon {
class Android;
class Camera;
class Keyboard;
class Mouse;
class TouchScreen;
class VirtualAmiibo;
class VirtualGamepad;
struct MappingData;
} // namespace InputCommon
namespace InputCommon::TasInput {
class Tas;
} // namespace InputCommon::TasInput
namespace InputCommon {
namespace Polling {
/// Type of input desired for mapping purposes
enum class InputType { None, Button, Stick, Motion, Touch };
} // namespace Polling
/**
* Given a ParamPackage for a Device returned from `GetInputDevices`, attempt to get the default
* mapping for the device.
*/
using AnalogMapping = std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage>;
using ButtonMapping = std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage>;
using MotionMapping = std::unordered_map<Settings::NativeMotion::Values, Common::ParamPackage>;
class InputSubsystem {
public:
explicit InputSubsystem();
~InputSubsystem();
InputSubsystem(const InputSubsystem&) = delete;
InputSubsystem& operator=(const InputSubsystem&) = delete;
InputSubsystem(InputSubsystem&&) = delete;
InputSubsystem& operator=(InputSubsystem&&) = delete;
/// Initializes and registers all built-in input device factories.
void Initialize();
/// Unregisters all built-in input device factories and shuts them down.
void Shutdown();
/// Retrieves the underlying keyboard device.
[[nodiscard]] Keyboard* GetKeyboard();
/// Retrieves the underlying keyboard device.
[[nodiscard]] const Keyboard* GetKeyboard() const;
/// Retrieves the underlying mouse device.
[[nodiscard]] Mouse* GetMouse();
/// Retrieves the underlying mouse device.
[[nodiscard]] const Mouse* GetMouse() const;
/// Retrieves the underlying touch screen device.
[[nodiscard]] TouchScreen* GetTouchScreen();
/// Retrieves the underlying touch screen device.
[[nodiscard]] const TouchScreen* GetTouchScreen() const;
/// Retrieves the underlying tas input device.
[[nodiscard]] TasInput::Tas* GetTas();
/// Retrieves the underlying tas input device.
[[nodiscard]] const TasInput::Tas* GetTas() const;
/// Retrieves the underlying camera input device.
[[nodiscard]] Camera* GetCamera();
/// Retrieves the underlying camera input device.
[[nodiscard]] const Camera* GetCamera() const;
/// Retrieves the underlying android input device.
[[nodiscard]] Android* GetAndroid();
/// Retrieves the underlying android input device.
[[nodiscard]] const Android* GetAndroid() const;
/// Retrieves the underlying virtual amiibo input device.
[[nodiscard]] VirtualAmiibo* GetVirtualAmiibo();
/// Retrieves the underlying virtual amiibo input device.
[[nodiscard]] const VirtualAmiibo* GetVirtualAmiibo() const;
/// Retrieves the underlying virtual gamepad input device.
[[nodiscard]] VirtualGamepad* GetVirtualGamepad();
/// Retrieves the underlying virtual gamepad input device.
[[nodiscard]] const VirtualGamepad* GetVirtualGamepad() const;
/**
* Returns all available input devices that this Factory can create a new device with.
* Each returned ParamPackage should have a `display` field used for display, a `engine` field
* for backends to determine if this backend is meant to service the request and any other
* information needed to identify this in the backend later.
*/
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const;
/// Retrieves the analog mappings for the given device.
[[nodiscard]] AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& device) const;
/// Retrieves the button mappings for the given device.
[[nodiscard]] ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& device) const;
/// Retrieves the motion mappings for the given device.
[[nodiscard]] MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& device) const;
/// Returns an enum containing the name to be displayed from the input engine.
[[nodiscard]] Common::Input::ButtonNames GetButtonName(
const Common::ParamPackage& params) const;
/// Returns true if device is a controller.
[[nodiscard]] bool IsController(const Common::ParamPackage& params) const;
/// Returns true if axis of a stick aren't mapped in the correct direction
[[nodiscard]] bool IsStickInverted(const Common::ParamPackage& device) const;
/// Reloads the input devices.
void ReloadInputDevices();
/// Start polling from all backends for a desired input type.
void BeginMapping(Polling::InputType type);
/// Returns an input event with mapping information.
[[nodiscard]] Common::ParamPackage GetNextInput() const;
/// Stop polling from all backends.
void StopMapping() const;
/// Signals SDL driver for new input events
void PumpEvents() const;
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
/// Generates a serialized param package for creating a keyboard button device.
std::string GenerateKeyboardParam(int key_code);
/// Generates a serialized param package for creating an analog device taking input from keyboard.
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
int key_modifier, float modifier_scale);
} // namespace InputCommon

View File

@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_precompiled_headers.h"