summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt7
-rw-r--r--src/common/thread.h9
-rw-r--r--src/core/settings.h3
-rw-r--r--src/input_common/CMakeLists.txt8
-rw-r--r--src/input_common/main.cpp12
-rw-r--r--src/input_common/udp/client.cpp287
-rw-r--r--src/input_common/udp/client.h96
-rw-r--r--src/input_common/udp/protocol.cpp79
-rw-r--r--src/input_common/udp/protocol.h256
-rw-r--r--src/input_common/udp/udp.cpp96
-rw-r--r--src/input_common/udp/udp.h27
-rw-r--r--src/yuzu/configuration/config.cpp17
-rw-r--r--src/yuzu_cmd/config.cpp5
-rw-r--r--src/yuzu_cmd/default_ini.h17
14 files changed, 915 insertions, 4 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 118572c03..dc782e252 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -350,6 +350,13 @@ function(create_target_directory_groups target_name)
endforeach()
endfunction()
+# Prevent boost from linking against libs when building
+add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY
+ -DBOOST_SYSTEM_NO_LIB
+ -DBOOST_DATE_TIME_NO_LIB
+ -DBOOST_REGEX_NO_LIB
+)
+
enable_testing()
add_subdirectory(externals)
add_subdirectory(src)
diff --git a/src/common/thread.h b/src/common/thread.h
index 0cfd98be6..2fc071685 100644
--- a/src/common/thread.h
+++ b/src/common/thread.h
@@ -28,6 +28,15 @@ public:
is_set = false;
}
+ template <class Duration>
+ bool WaitFor(const std::chrono::duration<Duration>& time) {
+ std::unique_lock lk{mutex};
+ if (!condvar.wait_for(lk, time, [this] { return is_set; }))
+ return false;
+ is_set = false;
+ return true;
+ }
+
template <class Clock, class Duration>
bool WaitUntil(const std::chrono::time_point<Clock, Duration>& time) {
std::unique_lock lk{mutex};
diff --git a/src/core/settings.h b/src/core/settings.h
index 9c98a9287..421e76f5f 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -401,6 +401,9 @@ struct Values {
std::string motion_device;
TouchscreenInput touchscreen;
std::atomic_bool is_device_reload_pending{true};
+ std::string udp_input_address;
+ u16 udp_input_port;
+ u8 udp_pad_index;
// Core
bool use_multi_core;
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt
index 5b4e032bd..2520ba321 100644
--- a/src/input_common/CMakeLists.txt
+++ b/src/input_common/CMakeLists.txt
@@ -9,6 +9,12 @@ add_library(input_common STATIC
motion_emu.h
sdl/sdl.cpp
sdl/sdl.h
+ udp/client.cpp
+ udp/client.h
+ udp/protocol.cpp
+ udp/protocol.h
+ udp/udp.cpp
+ udp/udp.h
)
if(SDL2_FOUND)
@@ -21,4 +27,4 @@ if(SDL2_FOUND)
endif()
create_target_directory_groups(input_common)
-target_link_libraries(input_common PUBLIC core PRIVATE common)
+target_link_libraries(input_common PUBLIC core PRIVATE common ${Boost_LIBRARIES})
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index 8e66c1b15..9e028da89 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -9,6 +9,7 @@
#include "input_common/keyboard.h"
#include "input_common/main.h"
#include "input_common/motion_emu.h"
+#include "input_common/udp/udp.h"
#ifdef HAVE_SDL2
#include "input_common/sdl/sdl.h"
#endif
@@ -18,6 +19,7 @@ namespace InputCommon {
static std::shared_ptr<Keyboard> keyboard;
static std::shared_ptr<MotionEmu> motion_emu;
static std::unique_ptr<SDL::State> sdl;
+static std::unique_ptr<CemuhookUDP::State> udp;
void Init() {
keyboard = std::make_shared<Keyboard>();
@@ -28,6 +30,8 @@ void Init() {
Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu);
sdl = SDL::Init();
+
+ udp = CemuhookUDP::Init();
}
void Shutdown() {
@@ -72,11 +76,13 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left,
namespace Polling {
std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) {
+ std::vector<std::unique_ptr<DevicePoller>> pollers;
+
#ifdef HAVE_SDL2
- return sdl->GetPollers(type);
-#else
- return {};
+ pollers = sdl->GetPollers(type);
#endif
+
+ return pollers;
}
} // namespace Polling
diff --git a/src/input_common/udp/client.cpp b/src/input_common/udp/client.cpp
new file mode 100644
index 000000000..5f5a9989c
--- /dev/null
+++ b/src/input_common/udp/client.cpp
@@ -0,0 +1,287 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <array>
+#include <chrono>
+#include <cstring>
+#include <functional>
+#include <thread>
+#include <boost/asio.hpp>
+#include <boost/bind.hpp>
+#include "common/logging/log.h"
+#include "input_common/udp/client.h"
+#include "input_common/udp/protocol.h"
+
+using boost::asio::ip::address_v4;
+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, u8 pad_index, u32 client_id,
+ SocketCallback callback)
+ : client_id(client_id), timer(io_service),
+ send_endpoint(udp::endpoint(address_v4::from_string(host), port)),
+ socket(io_service, udp::endpoint(udp::v4(), 0)), pad_index(pad_index),
+ callback(std::move(callback)) {}
+
+ 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:
+ void HandleReceive(const boost::system::error_code& error, 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& error) {
+ // Send a request for getting port info for the pad
+ Request::PortInfo port_info{1, {pad_index, 0, 0, 0}};
+ 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);
+
+ // Send a request for getting pad data for the pad
+ Request::PadData pad_data{Request::PadData::Flags::Id, pad_index, 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);
+ StartSend(timer.expiry());
+ }
+
+ SocketCallback callback;
+ boost::asio::io_service io_service;
+ boost::asio::basic_waitable_timer<clock> timer;
+ udp::socket socket;
+
+ u32 client_id{};
+ u8 pad_index{};
+
+ 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();
+}
+
+Client::Client(std::shared_ptr<DeviceStatus> status, const std::string& host, u16 port,
+ u8 pad_index, u32 client_id)
+ : status(status) {
+ StartCommunication(host, port, pad_index, client_id);
+}
+
+Client::~Client() {
+ socket->Stop();
+ thread.join();
+}
+
+void Client::ReloadSocket(const std::string& host, u16 port, u8 pad_index, u32 client_id) {
+ socket->Stop();
+ thread.join();
+ StartCommunication(host, port, pad_index, client_id);
+}
+
+void Client::OnVersion(Response::Version data) {
+ LOG_TRACE(Input, "Version packet received: {}", data.version);
+}
+
+void Client::OnPortInfo(Response::PortInfo data) {
+ LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
+}
+
+void Client::OnPadData(Response::PadData data) {
+ LOG_TRACE(Input, "PadData packet received");
+ if (data.packet_counter <= packet_sequence) {
+ LOG_WARNING(
+ Input,
+ "PadData packet dropped because its stale info. Current count: {} Packet count: {}",
+ packet_sequence, data.packet_counter);
+ return;
+ }
+ packet_sequence = data.packet_counter;
+ // TODO: Check how the Switch handles motions and how the CemuhookUDP motion
+ // directions correspond to the ones of the Switch
+ Common::Vec3f accel = Common::MakeVec<float>(data.accel.x, data.accel.y, data.accel.z);
+ Common::Vec3f gyro = Common::MakeVec<float>(data.gyro.pitch, data.gyro.yaw, data.gyro.roll);
+ {
+ std::lock_guard guard(status->update_mutex);
+
+ status->motion_status = {accel, gyro};
+
+ // TODO: add a setting for "click" touch. Click touch refers to a device that differentiates
+ // between a simple "tap" and a hard press that causes the touch screen to click.
+ const bool is_active = data.touch_1.is_active != 0;
+
+ float x = 0;
+ float y = 0;
+
+ if (is_active && status->touch_calibration) {
+ const u16 min_x = status->touch_calibration->min_x;
+ const u16 max_x = status->touch_calibration->max_x;
+ const u16 min_y = status->touch_calibration->min_y;
+ const u16 max_y = status->touch_calibration->max_y;
+
+ x = (std::clamp(static_cast<u16>(data.touch_1.x), min_x, max_x) - min_x) /
+ static_cast<float>(max_x - min_x);
+ y = (std::clamp(static_cast<u16>(data.touch_1.y), min_y, max_y) - min_y) /
+ static_cast<float>(max_y - min_y);
+ }
+
+ status->touch_status = {x, y, is_active};
+ }
+}
+
+void Client::StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id) {
+ SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
+ [this](Response::PortInfo info) { OnPortInfo(info); },
+ [this](Response::PadData data) { OnPadData(data); }};
+ LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
+ socket = std::make_unique<Socket>(host, port, pad_index, client_id, callback);
+ thread = std::thread{SocketLoop, this->socket.get()};
+}
+
+void TestCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id,
+ std::function<void()> success_callback,
+ std::function<void()> failure_callback) {
+ std::thread([=] {
+ Common::Event success_event;
+ SocketCallback callback{[](Response::Version version) {}, [](Response::PortInfo info) {},
+ [&](Response::PadData data) { success_event.Set(); }};
+ Socket socket{host, port, pad_index, client_id, callback};
+ std::thread worker_thread{SocketLoop, &socket};
+ bool result = success_event.WaitFor(std::chrono::seconds(8));
+ socket.Stop();
+ worker_thread.join();
+ if (result) {
+ success_callback();
+ } else {
+ failure_callback();
+ }
+ })
+ .detach();
+}
+
+CalibrationConfigurationJob::CalibrationConfigurationJob(
+ const std::string& host, u16 port, u8 pad_index, u32 client_id,
+ std::function<void(Status)> status_callback,
+ std::function<void(u16, u16, u16, u16)> data_callback) {
+
+ std::thread([=] {
+ constexpr u16 CALIBRATION_THRESHOLD = 100;
+
+ 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 version) {}, [](Response::PortInfo info) {},
+ [&](Response::PadData data) {
+ if (current_status == Status::Initialized) {
+ // Receiving data means the communication is ready now
+ current_status = Status::Ready;
+ status_callback(current_status);
+ }
+ if (!data.touch_1.is_active) {
+ return;
+ }
+ LOG_DEBUG(Input, "Current touch: {} {}", data.touch_1.x,
+ data.touch_1.y);
+ min_x = std::min(min_x, static_cast<u16>(data.touch_1.x));
+ min_y = std::min(min_y, static_cast<u16>(data.touch_1.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_1.x - min_x > CALIBRATION_THRESHOLD &&
+ data.touch_1.y - min_y > CALIBRATION_THRESHOLD) {
+ // Set the current position as max value and finishes
+ // configuration
+ max_x = data.touch_1.x;
+ max_y = data.touch_1.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, pad_index, client_id, 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
diff --git a/src/input_common/udp/client.h b/src/input_common/udp/client.h
new file mode 100644
index 000000000..0b21f4da6
--- /dev/null
+++ b/src/input_common/udp/client.h
@@ -0,0 +1,96 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <thread>
+#include <tuple>
+#include <vector>
+#include "common/common_types.h"
+#include "common/thread.h"
+#include "common/vector_math.h"
+
+namespace InputCommon::CemuhookUDP {
+
+constexpr u16 DEFAULT_PORT = 26760;
+constexpr char DEFAULT_ADDR[] = "127.0.0.1";
+
+class Socket;
+
+namespace Response {
+struct PadData;
+struct PortInfo;
+struct Version;
+} // namespace Response
+
+struct DeviceStatus {
+ std::mutex update_mutex;
+ std::tuple<Common::Vec3<float>, Common::Vec3<float>> motion_status;
+ std::tuple<float, float, bool> touch_status;
+
+ // 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;
+};
+
+class Client {
+public:
+ explicit Client(std::shared_ptr<DeviceStatus> status, const std::string& host = DEFAULT_ADDR,
+ u16 port = DEFAULT_PORT, u8 pad_index = 0, u32 client_id = 24872);
+ ~Client();
+ void ReloadSocket(const std::string& host = "127.0.0.1", u16 port = 26760, u8 pad_index = 0,
+ u32 client_id = 24872);
+
+private:
+ void OnVersion(Response::Version);
+ void OnPortInfo(Response::PortInfo);
+ void OnPadData(Response::PadData);
+ void StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id);
+
+ std::unique_ptr<Socket> socket;
+ std::shared_ptr<DeviceStatus> status;
+ std::thread thread;
+ u64 packet_sequence = 0;
+};
+
+/// 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, u8 pad_index,
+ u32 client_id, 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, u8 pad_index, u32 client_id,
+ std::function<void()> success_callback,
+ std::function<void()> failure_callback);
+
+} // namespace InputCommon::CemuhookUDP
diff --git a/src/input_common/udp/protocol.cpp b/src/input_common/udp/protocol.cpp
new file mode 100644
index 000000000..a982ac49d
--- /dev/null
+++ b/src/input_common/udp/protocol.cpp
@@ -0,0 +1,79 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <cstddef>
+#include <cstring>
+#include "common/logging/log.h"
+#include "input_common/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 thats the easiest way to check without
+ * copying the buffer)
+ */
+std::optional<Type> Validate(u8* data, std::size_t size) {
+ if (size < sizeof(Header)) {
+ LOG_DEBUG(Input, "Invalid UDP packet received");
+ 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
diff --git a/src/input_common/udp/protocol.h b/src/input_common/udp/protocol.h
new file mode 100644
index 000000000..1b521860a
--- /dev/null
+++ b/src/input_common/udp/protocol.h
@@ -0,0 +1,256 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <optional>
+#include <type_traits>
+#include <vector>
+#include <boost/crc.hpp>
+#include "common/bit_field.h"
+#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();
+
+namespace Request {
+
+struct Version {};
+/**
+ * Requests the server to send information about what controllers are plugged into the ports
+ * In citra'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 {
+ enum class Flags : u8 {
+ AllPorts,
+ Id,
+ Mac,
+ };
+ /// Determines which method will be used as a look up for the controller
+ Flags 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 T 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) {
+ boost::crc_32_type crc;
+ Header header{
+ CLIENT_MAGIC, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, client_id, GetMessageType<T>(),
+ };
+ Message<T> message{header, data};
+ crc.process_bytes(&message, sizeof(Message<T>));
+ message.header.crc = crc.checksum();
+ return message;
+}
+} // namespace Request
+
+namespace Response {
+
+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{};
+ u8 state{};
+ u8 model{};
+ u8 connection_type{};
+ MacAddress mac;
+ u8 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");
+
+#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_8{};
+ u8 button_7{};
+ u8 button_6{};
+ u8 button_5{};
+ u8 button_12{};
+ u8 button_11{};
+ u8 button_10{};
+ u8 button_9{};
+ u8 button_16{};
+ u8 button_15{};
+ u8 button_14{};
+ u8 button_13{};
+ } analog_button;
+
+ struct TouchPad {
+ u8 is_active{};
+ u8 id{};
+ u16_le x{};
+ u16_le y{};
+ } touch_1, touch_2;
+
+ 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::TouchPad) == 6, "UDP Response TouchPad 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
diff --git a/src/input_common/udp/udp.cpp b/src/input_common/udp/udp.cpp
new file mode 100644
index 000000000..a80f38614
--- /dev/null
+++ b/src/input_common/udp/udp.cpp
@@ -0,0 +1,96 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/logging/log.h"
+#include "common/param_package.h"
+#include "core/frontend/input.h"
+#include "core/settings.h"
+#include "input_common/udp/client.h"
+#include "input_common/udp/udp.h"
+
+namespace InputCommon::CemuhookUDP {
+
+class UDPTouchDevice final : public Input::TouchDevice {
+public:
+ explicit UDPTouchDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
+ std::tuple<float, float, bool> GetStatus() const {
+ std::lock_guard guard(status->update_mutex);
+ return status->touch_status;
+ }
+
+private:
+ std::shared_ptr<DeviceStatus> status;
+};
+
+class UDPMotionDevice final : public Input::MotionDevice {
+public:
+ explicit UDPMotionDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
+ std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const {
+ std::lock_guard guard(status->update_mutex);
+ return status->motion_status;
+ }
+
+private:
+ std::shared_ptr<DeviceStatus> status;
+};
+
+class UDPTouchFactory final : public Input::Factory<Input::TouchDevice> {
+public:
+ explicit UDPTouchFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
+
+ std::unique_ptr<Input::TouchDevice> Create(const Common::ParamPackage& params) override {
+ {
+ std::lock_guard guard(status->update_mutex);
+ status->touch_calibration.emplace();
+ // These default values work well for DS4 but probably not other touch inputs
+ status->touch_calibration->min_x = params.Get("min_x", 100);
+ status->touch_calibration->min_y = params.Get("min_y", 50);
+ status->touch_calibration->max_x = params.Get("max_x", 1800);
+ status->touch_calibration->max_y = params.Get("max_y", 850);
+ }
+ return std::make_unique<UDPTouchDevice>(status);
+ }
+
+private:
+ std::shared_ptr<DeviceStatus> status;
+};
+
+class UDPMotionFactory final : public Input::Factory<Input::MotionDevice> {
+public:
+ explicit UDPMotionFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
+
+ std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override {
+ return std::make_unique<UDPMotionDevice>(status);
+ }
+
+private:
+ std::shared_ptr<DeviceStatus> status;
+};
+
+State::State() {
+ auto status = std::make_shared<DeviceStatus>();
+ client =
+ std::make_unique<Client>(status, Settings::values.udp_input_address,
+ Settings::values.udp_input_port, Settings::values.udp_pad_index);
+
+ Input::RegisterFactory<Input::TouchDevice>("cemuhookudp",
+ std::make_shared<UDPTouchFactory>(status));
+ Input::RegisterFactory<Input::MotionDevice>("cemuhookudp",
+ std::make_shared<UDPMotionFactory>(status));
+}
+
+State::~State() {
+ Input::UnregisterFactory<Input::TouchDevice>("cemuhookudp");
+ Input::UnregisterFactory<Input::MotionDevice>("cemuhookudp");
+}
+
+void State::ReloadUDPClient() {
+ client->ReloadSocket(Settings::values.udp_input_address, Settings::values.udp_input_port,
+ Settings::values.udp_pad_index);
+}
+
+std::unique_ptr<State> Init() {
+ return std::make_unique<State>();
+}
+} // namespace InputCommon::CemuhookUDP
diff --git a/src/input_common/udp/udp.h b/src/input_common/udp/udp.h
new file mode 100644
index 000000000..ea3de60bb
--- /dev/null
+++ b/src/input_common/udp/udp.h
@@ -0,0 +1,27 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <memory>
+#include <unordered_map>
+#include "input_common/main.h"
+#include "input_common/udp/client.h"
+
+namespace InputCommon::CemuhookUDP {
+
+class UDPTouchDevice;
+class UDPMotionDevice;
+
+class State {
+public:
+ State();
+ ~State();
+ void ReloadUDPClient();
+
+private:
+ std::unique_ptr<Client> client;
+};
+
+std::unique_ptr<State> Init();
+
+} // namespace InputCommon::CemuhookUDP
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index f92a4b3c3..59918847a 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -10,6 +10,7 @@
#include "core/hle/service/acc/profile_manager.h"
#include "core/hle/service/hid/controllers/npad.h"
#include "input_common/main.h"
+#include "input_common/udp/client.h"
#include "yuzu/configuration/config.h"
#include "yuzu/uisettings.h"
@@ -429,6 +430,16 @@ void Config::ReadControlValues() {
QStringLiteral("engine:motion_emu,update_period:100,sensitivity:0.01"))
.toString()
.toStdString();
+ Settings::values.udp_input_address =
+ ReadSetting(QStringLiteral("udp_input_address"),
+ QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR))
+ .toString()
+ .toStdString();
+ Settings::values.udp_input_port = static_cast<u16>(
+ ReadSetting(QStringLiteral("udp_input_port"), InputCommon::CemuhookUDP::DEFAULT_PORT)
+ .toInt());
+ Settings::values.udp_pad_index =
+ static_cast<u8>(ReadSetting(QStringLiteral("udp_pad_index"), 0).toUInt());
qt_config->endGroup();
}
@@ -911,6 +922,12 @@ void Config::SaveControlValues() {
QString::fromStdString(Settings::values.motion_device),
QStringLiteral("engine:motion_emu,update_period:100,sensitivity:0.01"));
WriteSetting(QStringLiteral("keyboard_enabled"), Settings::values.keyboard_enabled, false);
+ WriteSetting(QStringLiteral("udp_input_address"),
+ QString::fromStdString(Settings::values.udp_input_address),
+ QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR));
+ WriteSetting(QStringLiteral("udp_input_port"), Settings::values.udp_input_port,
+ InputCommon::CemuhookUDP::DEFAULT_PORT);
+ WriteSetting(QStringLiteral("udp_pad_index"), Settings::values.udp_pad_index, 0);
qt_config->endGroup();
}
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index 1a812cb87..161583b54 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -12,6 +12,7 @@
#include "core/hle/service/acc/profile_manager.h"
#include "core/settings.h"
#include "input_common/main.h"
+#include "input_common/udp/client.h"
#include "yuzu_cmd/config.h"
#include "yuzu_cmd/default_ini.h"
@@ -297,6 +298,10 @@ void Config::ReadValues() {
sdl2_config->GetInteger("ControlsGeneral", "touch_diameter_x", 15);
Settings::values.touchscreen.diameter_y =
sdl2_config->GetInteger("ControlsGeneral", "touch_diameter_y", 15);
+ Settings::values.udp_input_address =
+ sdl2_config->Get("Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR);
+ Settings::values.udp_input_port = static_cast<u16>(sdl2_config->GetInteger(
+ "Controls", "udp_input_port", InputCommon::CemuhookUDP::DEFAULT_PORT));
std::transform(keyboard_keys.begin(), keyboard_keys.end(),
Settings::values.keyboard_keys.begin(), InputCommon::GenerateKeyboardParam);
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h
index 8d18a4a5a..e829f8695 100644
--- a/src/yuzu_cmd/default_ini.h
+++ b/src/yuzu_cmd/default_ini.h
@@ -69,12 +69,29 @@ rstick=
# - "motion_emu" (default) for emulating motion input from mouse input. Required parameters:
# - "update_period": update period in milliseconds (default to 100)
# - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01)
+# - "cemuhookudp" reads motion input from a udp server that uses cemuhook's udp protocol
motion_device=
# for touch input, the following devices are available:
# - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required
+# - "cemuhookudp" reads touch input from a udp server that uses cemuhook's udp protocol
+# - "min_x", "min_y", "max_x", "max_y": defines the udp device's touch screen coordinate system
touch_device=
+# Most desktop operating systems do not expose a way to poll the motion state of the controllers
+# so as a way around it, cemuhook created a udp client/server protocol to broadcast the data directly
+# from a controller device to the client program. Citra has a client that can connect and read
+# from any cemuhook compatible motion program.
+
+# IPv4 address of the udp input server (Default "127.0.0.1")
+udp_input_address=
+
+# Port of the udp input server. (Default 26760)
+udp_input_port=
+
+# The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0)
+udp_pad_index=
+
[Core]
# Whether to use multi-core for CPU emulation
# 0 (default): Disabled, 1: Enabled