diff --git a/README.md b/README.md index 8d3c466..3471cf2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # inputtino An easy to use virtual input library for Linux built on top of `uinput`, `evdev` and `uhid`. +Currently in use by [Wolf](https://github.com/games-on-whales/wolf), [Sunshine](https://github.com/LizardByte/Sunshine) and [Moonshine](https://github.com/hgaiser/moonshine) + Supports: - Keyboard @@ -11,13 +13,15 @@ Supports: - Joypad - Correctly emulates Xbox, PS5 or Nintendo joypads - Supports callbacks on Rumble events - - Gyro, Acceleration and Touchpad support (using UHID) + - Gyro, Acceleration, Touchpad, Adaptive triggers, LED and battery status fully supported when creating a virtual DualSense joypad (with full support without Steam Input for games that are compatible with DualSense) Interested in how the joypad works under the hood? Checkout these blog posts: - [When uinput Isn’t Enough: Virtualizing a DualSense controller](https://abeltra.me/blog/inputtino-uhid-1/) - [Creating a Virtual DualSense Controller via UHID](https://abeltra.me/blog/inputtino-uhid-2/) - [Beyond USB: Improving Virtual Controller Support in Linux Games](https://abeltra.me/blog/inputtino-uhid-3/) +A special thanks goes to [@hgaiser](https://github.com/hgaiser) for all the help in yak shaving the DualSense implementation. + ## Include in a C++ project If using `Cmake` it's as simple as @@ -31,7 +35,7 @@ FetchContent_MakeAvailable(inputtino) target_link_libraries( PUBLIC inputtino::libinputtino) ``` -## Example usage +### Example usage ```c++ #include diff --git a/bindings/rust/inputtino/src/joypad_ps5.rs b/bindings/rust/inputtino/src/joypad_ps5.rs index 73b70e5..3f53b40 100644 --- a/bindings/rust/inputtino/src/joypad_ps5.rs +++ b/bindings/rust/inputtino/src/joypad_ps5.rs @@ -8,7 +8,7 @@ use crate::sys::{ inputtino_joypad_ps5_set_on_led, inputtino_joypad_ps5_set_on_rumble, inputtino_joypad_ps5_set_pressed_buttons, inputtino_joypad_ps5_set_stick, inputtino_joypad_ps5_set_triggers, inputtino_joypad_ps5_set_motion, - inputtino_joypad_ps5_set_battery, + inputtino_joypad_ps5_set_battery, inputtino_joypad_ps5_set_on_trigger_effect, }; use crate::{BatteryState, InputtinoError, JoypadMotionType, JoypadStickPosition}; @@ -17,6 +17,7 @@ pub struct PS5Joypad { joypad: *mut crate::sys::InputtinoPS5Joypad, on_rumble_fn: *mut c_void, on_led_fn: *mut c_void, + on_trigger_effect_fn: *mut c_void, } impl PS5Joypad { @@ -44,6 +45,7 @@ impl PS5Joypad { joypad, on_rumble_fn: std::ptr::null_mut(), on_led_fn: std::ptr::null_mut(), + on_trigger_effect_fn: std::ptr::null_mut(), }) } @@ -130,6 +132,29 @@ impl PS5Joypad { } } + /// Sets a callback to be called when this device receives a trigger effect event. + /// + /// # Examples + /// + /// ```ignore + /// device.set_on_trigger_effect(|trigger_event_flags, type_left, type_right, left_effect, right_effect| { + /// println!( + /// "Received trigger effect event: trigger_event_flags: {trigger_event_flags}, type_left: {type_left}, type_right: {type_right}, \ + /// left_effect: {:?}, right_effect: {:?}", + /// left_effect, right_effect + /// ); + /// }); + /// ``` + pub fn set_on_trigger_effect(&mut self, on_trigger_effect_fn: impl FnMut(u8, u8, u8, &[u8], &[u8]) + 'static) { + let on_trigger_effect_fn = Box::new(TriggerEffectFunction { + on_trigger_effect_fn: Box::new(on_trigger_effect_fn), + }); + self.on_trigger_effect_fn = Box::into_raw(on_trigger_effect_fn) as *mut c_void; + unsafe { + inputtino_joypad_ps5_set_on_trigger_effect(self.joypad, Some(on_trigger_effect_c_fn), self.on_trigger_effect_fn); + } + } + pub fn get_nodes(&self) -> Result, InputtinoError> { get_nodes(inputtino_joypad_ps5_get_nodes, self.joypad) } @@ -205,10 +230,6 @@ struct RumbleFunction { on_rumble_fn: Box, } -struct LedFunction { - on_led_fn: Box, -} - unsafe extern "C" fn on_rumble_c_fn( left_motor: c_int, right_motor: c_int, @@ -218,6 +239,10 @@ unsafe extern "C" fn on_rumble_c_fn( ((*on_rumble_fn).on_rumble_fn)(left_motor, right_motor); } +struct LedFunction { + on_led_fn: Box, +} + unsafe extern "C" fn on_led_c_fn( r: c_int, g: c_int, @@ -228,4 +253,28 @@ unsafe extern "C" fn on_led_c_fn( ((*on_led_fn).on_led_fn)(r, g, b); } +struct TriggerEffectFunction { + on_trigger_effect_fn: Box, +} + +unsafe extern "C" fn on_trigger_effect_c_fn( + trigger_event_flags: u8, + type_left: u8, + type_right: u8, + left: *const u8, + right: *const u8, + user_data: *mut ::core::ffi::c_void, +) { + let on_trigger_effect_fn = user_data as *mut TriggerEffectFunction; + let left_effect = std::slice::from_raw_parts(left, 10); + let right_effect = std::slice::from_raw_parts(right, 10); + ((*on_trigger_effect_fn).on_trigger_effect_fn)( + trigger_event_flags, + type_left, + type_right, + left_effect, + right_effect, + ); +} + unsafe impl Send for PS5Joypad {} diff --git a/include/inputtino/input.h b/include/inputtino/input.h index 2d704e5..3ebf632 100644 --- a/include/inputtino/input.h +++ b/include/inputtino/input.h @@ -10,6 +10,7 @@ # include #endif #include +#include #ifdef __cplusplus extern "C" { @@ -309,6 +310,11 @@ typedef void (*InputtinoJoypadLEDFn)(int r, int g, int b, void *user_data); LIBINPUTTINO_EXPORT void inputtino_joypad_ps5_set_on_led(InputtinoPS5Joypad *joypad, InputtinoJoypadLEDFn led_fn, void *user_data); +typedef void (*InputtinoJoypadTriggerFn)(uint8_t type_left, + uint8_t event_flags, uint8_t type_right, const uint8_t *left, const uint8_t *right, void *user_data); + +LIBINPUTTINO_EXPORT void inputtino_joypad_ps5_set_on_trigger_effect(InputtinoPS5Joypad *joypad, InputtinoJoypadTriggerFn trigger_effect_fn, void *user_data); + LIBINPUTTINO_EXPORT void inputtino_joypad_ps5_destroy(InputtinoPS5Joypad *joypad); #ifdef __cplusplus diff --git a/include/inputtino/input.hpp b/include/inputtino/input.hpp index 7b399b6..10a1ffc 100644 --- a/include/inputtino/input.hpp +++ b/include/inputtino/input.hpp @@ -428,6 +428,25 @@ class PS5Joypad : public Joypad { void set_on_led(const std::function &callback); + /** + * This is an opaque blob that is sent to the controller. + * There is some reversed engineered information here: + * https://gist.github.com/Nielk1/6d54cc2c00d2201ccb8c2720ad7538db + */ + struct TriggerEffect { + /** + * 0x04 - Right trigger + * 0x08 - Left trigger + */ + uint8_t event_flags; + uint8_t type_left; + uint8_t type_right; + std::array left = {}; + std::array right = {}; + }; + + void set_on_trigger_effect(const std::function &callback); + protected: typedef struct PS5JoypadState PS5JoypadState; std::shared_ptr _state; diff --git a/src/c-bindings/joypad_ps5.cpp b/src/c-bindings/joypad_ps5.cpp index 3e7e196..df74a54 100644 --- a/src/c-bindings/joypad_ps5.cpp +++ b/src/c-bindings/joypad_ps5.cpp @@ -91,6 +91,22 @@ void inputtino_joypad_ps5_set_on_led(InputtinoPS5Joypad *joypad, InputtinoJoypad } } +void inputtino_joypad_ps5_set_on_trigger_effect(InputtinoPS5Joypad *joypad, + InputtinoJoypadTriggerFn trigger_effect_fn, + void *user_data) { + if (joypad) { + reinterpret_cast(joypad)->set_on_trigger_effect( + [user_data, trigger_effect_fn](const inputtino::PS5Joypad::TriggerEffect &effect) { + trigger_effect_fn(effect.event_flags, + effect.type_left, + effect.type_right, + effect.left.data(), + effect.right.data(), + user_data); + }); + } +} + void inputtino_joypad_ps5_destroy(InputtinoPS5Joypad *joypad) { if (joypad) { auto joypad_ptr = reinterpret_cast(joypad); diff --git a/src/uhid/include/uhid/protected_types.hpp b/src/uhid/include/uhid/protected_types.hpp index 02ad79d..effe6d5 100644 --- a/src/uhid/include/uhid/protected_types.hpp +++ b/src/uhid/include/uhid/protected_types.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -25,6 +26,10 @@ struct PS5JoypadState { std::optional> on_rumble = std::nullopt; std::optional> on_led = std::nullopt; + std::optional> on_trigger_effect = std::nullopt; + uint32_t last_left_trigger_event = 0; + uint32_t last_right_trigger_event = 0; + bool stop_repeat_thread = false; bool is_bluetooth = true; }; diff --git a/src/uhid/include/uhid/ps5.hpp b/src/uhid/include/uhid/ps5.hpp index cd65061..dd2561c 100644 --- a/src/uhid/include/uhid/ps5.hpp +++ b/src/uhid/include/uhid/ps5.hpp @@ -22,7 +22,7 @@ static constexpr int PS5_GYRO_RES_PER_DEG_S = 1024; static constexpr int PS5_GYRO_RANGE = (2048 * PS5_GYRO_RES_PER_DEG_S); static constexpr int PS5_TOUCHPAD_WIDTH = 1920; static constexpr int PS5_TOUCHPAD_HEIGHT = 1080; -static constexpr float SDL_STANDARD_GRAVITY = 9.80665f; +static constexpr float SDL_STANDARD_GRAVITY_CONST = 9.80665f; /* * Bluetooth constants, @@ -472,8 +472,9 @@ struct dualsense_input_report { enum FLAG0 : uint8_t { MOTOR_OR_COMPATIBLE_VIBRATION = 0x01, - LED_OR_HAPTIC_SELECT = 0x02, - LED_BLINK = 0x04 + USE_RUMBLE_NOT_HAPTICS = 0x02, + RIGHT_TRIGGER_EFFECT = 0x04, + LEFT_TRIGGER_EFFECT = 0x08, }; enum FLAG1 : uint8_t { @@ -503,7 +504,11 @@ struct dualsense_output_report_common { uint8_t mute_button_led; uint8_t power_save_control; - uint8_t reserved2[28]; + uint8_t right_trigger_effect_type; + uint8_t right_trigger_effect[10]; + uint8_t left_trigger_effect_type; + uint8_t left_trigger_effect[10]; + uint8_t reserved2[6]; /* LEDs and lightbar */ uint8_t valid_flag2; diff --git a/src/uhid/joypad_ps5.cpp b/src/uhid/joypad_ps5.cpp index fe7acb0..96a5a92 100644 --- a/src/uhid/joypad_ps5.cpp +++ b/src/uhid/joypad_ps5.cpp @@ -168,6 +168,39 @@ static void on_uhid_event(std::shared_ptr state, uhid_event ev, } } + /** + * Trigger effects + */ + bool right_trigger = report.valid_flag0 & uhid::RIGHT_TRIGGER_EFFECT; + bool left_trigger = report.valid_flag0 & uhid::LEFT_TRIGGER_EFFECT; + if ((right_trigger || left_trigger) && state->on_trigger_effect) { + auto left_array_start = std::begin(report.left_trigger_effect); + auto left_array_end = std::end(report.left_trigger_effect); + auto right_array_start = std::begin(report.right_trigger_effect); + auto right_array_end = std::end(report.right_trigger_effect); + // We have to cache these values because these flags will be set as long as the effect is active + uint32_t left_trigger_hash = std::accumulate(left_array_start, left_array_end, 0ul); + uint32_t right_trigger_hash = std::accumulate(right_array_start, right_array_end, 0ul); + if ((left_trigger && state->last_left_trigger_event != left_trigger_hash) || + (right_trigger && state->last_right_trigger_event != right_trigger_hash)) { + // First, update the cache + if (left_trigger) + state->last_left_trigger_event = left_trigger_hash; + if (right_trigger) + state->last_right_trigger_event = right_trigger_hash; + + // Then, trigger the event + uint8_t event_flags = (report.valid_flag0 & uhid::LEFT_TRIGGER_EFFECT) | + (report.valid_flag0 & uhid::RIGHT_TRIGGER_EFFECT); + PS5Joypad::TriggerEffect effect = {.event_flags = event_flags, + .type_left = report.left_trigger_effect_type, + .type_right = report.right_trigger_effect_type}; + std::copy(left_array_start, left_array_end, std::begin(effect.left)); + std::copy(right_array_start, right_array_end, std::begin(effect.right)); + (*state->on_trigger_effect)(effect); + } + } + /* * LED */ @@ -472,9 +505,9 @@ static __le16 to_le_signed(float original, float value) { void PS5Joypad::set_motion(PS5Joypad::MOTION_TYPE type, float x, float y, float z) { switch (type) { case ACCELERATION: { - this->_state->current_state.accel[0] = to_le_signed(x, (x * uhid::SDL_STANDARD_GRAVITY * 100)); - this->_state->current_state.accel[1] = to_le_signed(y, (y * uhid::SDL_STANDARD_GRAVITY * 100)); - this->_state->current_state.accel[2] = to_le_signed(z, (z * uhid::SDL_STANDARD_GRAVITY * 100)); + this->_state->current_state.accel[0] = to_le_signed(x, (x * uhid::SDL_STANDARD_GRAVITY_CONST * 100)); + this->_state->current_state.accel[1] = to_le_signed(y, (y * uhid::SDL_STANDARD_GRAVITY_CONST * 100)); + this->_state->current_state.accel[2] = to_le_signed(z, (z * uhid::SDL_STANDARD_GRAVITY_CONST * 100)); send_report(*this->_state); break; @@ -504,6 +537,10 @@ void PS5Joypad::set_on_led(const std::function &callback) { this->_state->on_led = callback; } +void PS5Joypad::set_on_trigger_effect(const std::function &callback) { + this->_state->on_trigger_effect = callback; +} + void PS5Joypad::place_finger(int finger_nr, uint16_t x, uint16_t y) { if (finger_nr <= 1) { // If this finger was previously unpressed, we should increase the touch id diff --git a/tests/testCAPI.cpp b/tests/testCAPI.cpp index 477b206..eb7874e 100644 --- a/tests/testCAPI.cpp +++ b/tests/testCAPI.cpp @@ -204,6 +204,15 @@ TEST_CASE("C PS5 API", "[C-API]") { inputtino_joypad_ps5_set_motion(ps_pad, INPUTTINO_JOYPAD_MOTION_TYPE::ACCELERATION, 1, 1, 1); inputtino_joypad_ps5_set_battery(ps_pad, BATTERY_STATE::BATTERY_DISCHARGING, 90); inputtino_joypad_ps5_set_on_led(ps_pad, [](int r, int g, int b, void *user_data) {}, nullptr); + inputtino_joypad_ps5_set_on_trigger_effect( + ps_pad, + [](uint8_t type_left, + uint8_t event_flags, + uint8_t type_right, + const uint8_t *left, + const uint8_t *right, + void *user_data) {}, + nullptr); } delete[] nodes; diff --git a/tests/testJoypads.cpp b/tests/testJoypads.cpp index 89a7b31..57cf62b 100644 --- a/tests/testJoypads.cpp +++ b/tests/testJoypads.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using Catch::Matchers::Contains; using Catch::Matchers::ContainsSubstring; @@ -402,6 +403,40 @@ TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL],[PS]") { } } + // Adaptive triggers aren't directly supported by SDL + // see:https://github.com/libsdl-org/SDL/issues/5125#issuecomment-1204261666 + // see: HIDAPI_DriverPS5_RumbleJoystickTriggers() + // but we can send custom data to the device, the following code is adapted from + // https://github.com/libsdl-org/SDL/blob/d66483dfccfcdc4e03f719e318c7a76f963f22d9/test/testcontroller.c#L235-L255 + { + auto trigger_event = std::make_shared(); + joypad.set_on_trigger_effect([trigger_event](const PS5Joypad::TriggerEffect &effect) { *trigger_event = effect; }); + + /* Resistance and vibration when trigger is pulled */ + uint8_t left_effect_type = 0x06; + uint8_t left_effect[10] = {15, 63, 128, 0, 0, 0, 0, 0, 0, 0}; + /* Constant resistance across entire trigger pull */ + uint8_t right_effect_type = 0x01; + uint8_t right_effect[10] = {0, 110, 0, 0, 0, 0, 0, 0, 0, 0}; + + uhid::dualsense_output_report_common state = {}; + SDL_zero(state); + state.valid_flag0 |= (uhid::RIGHT_TRIGGER_EFFECT); + state.right_trigger_effect_type = right_effect_type; + SDL_memcpy(state.right_trigger_effect, right_effect, sizeof(right_effect)); + state.left_trigger_effect_type = left_effect_type; + SDL_memcpy(state.left_trigger_effect, left_effect, sizeof(left_effect)); + SDL_GameControllerSendEffect(gc, &state, sizeof(state)); + + std::this_thread::sleep_for(15ms); + flush_sdl_events(); + REQUIRE(trigger_event->event_flags == uhid::RIGHT_TRIGGER_EFFECT); + REQUIRE(trigger_event->type_left == left_effect_type); + REQUIRE(trigger_event->type_right == right_effect_type); + REQUIRE(std::equal(std::begin(trigger_event->left), std::end(trigger_event->left), std::begin(left_effect))); + REQUIRE(std::equal(std::begin(trigger_event->right), std::end(trigger_event->right), std::begin(right_effect))); + } + { // Test creating a second device REQUIRE(SDL_NumJoysticks() == 1); auto joypad2 = std::move(*PS5Joypad::create()); @@ -419,10 +454,6 @@ TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL],[PS]") { SDL_GameControllerClose(gc2); } - // Adaptive triggers aren't supported by SDL - // see:https://github.com/libsdl-org/SDL/issues/5125#issuecomment-1204261666 - // see: HIDAPI_DriverPS5_RumbleJoystickTriggers() - SDL_GameControllerClose(gc); }