Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -31,7 +35,7 @@ FetchContent_MakeAvailable(inputtino)
target_link_libraries(<your_project_name> PUBLIC inputtino::libinputtino)
```

## Example usage
### Example usage

```c++
#include <inputtino/input.hpp>
Expand Down
59 changes: 54 additions & 5 deletions bindings/rust/inputtino/src/joypad_ps5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
})
}

Expand Down Expand Up @@ -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<Vec<PathBuf>, InputtinoError> {
get_nodes(inputtino_joypad_ps5_get_nodes, self.joypad)
}
Expand Down Expand Up @@ -205,10 +230,6 @@ struct RumbleFunction {
on_rumble_fn: Box<dyn FnMut(i32, i32)>,
}

struct LedFunction {
on_led_fn: Box<dyn FnMut(i32, i32, i32)>,
}

unsafe extern "C" fn on_rumble_c_fn(
left_motor: c_int,
right_motor: c_int,
Expand All @@ -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<dyn FnMut(i32, i32, i32)>,
}

unsafe extern "C" fn on_led_c_fn(
r: c_int,
g: c_int,
Expand All @@ -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<dyn FnMut(u8, u8, u8, &[u8], &[u8])>,
}

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 {}
6 changes: 6 additions & 0 deletions include/inputtino/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# include <inputtino/export_static.h>
#endif
#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions include/inputtino/input.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,25 @@ class PS5Joypad : public Joypad {

void set_on_led(const std::function<void(int r, int g, int b)> &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<uint8_t, 10> left = {};
std::array<uint8_t, 10> right = {};
};

void set_on_trigger_effect(const std::function<void(const TriggerEffect &)> &callback);

protected:
typedef struct PS5JoypadState PS5JoypadState;
std::shared_ptr<PS5JoypadState> _state;
Expand Down
16 changes: 16 additions & 0 deletions src/c-bindings/joypad_ps5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<inputtino::PS5Joypad *>(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<inputtino::PS5Joypad *>(joypad);
Expand Down
5 changes: 5 additions & 0 deletions src/uhid/include/uhid/protected_types.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include <functional>
#include <inputtino/input.hpp>
#include <optional>
#include <uhid/ps5.hpp>
#include <uhid/uhid.hpp>
Expand All @@ -25,6 +26,10 @@ struct PS5JoypadState {

std::optional<std::function<void(int, int)>> on_rumble = std::nullopt;
std::optional<std::function<void(int, int, int)>> on_led = std::nullopt;
std::optional<std::function<void(const PS5Joypad::TriggerEffect &)>> 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;
};
Expand Down
13 changes: 9 additions & 4 deletions src/uhid/include/uhid/ps5.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
43 changes: 40 additions & 3 deletions src/uhid/joypad_ps5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,39 @@ static void on_uhid_event(std::shared_ptr<PS5JoypadState> 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
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -504,6 +537,10 @@ void PS5Joypad::set_on_led(const std::function<void(int, int, int)> &callback) {
this->_state->on_led = callback;
}

void PS5Joypad::set_on_trigger_effect(const std::function<void(const TriggerEffect &)> &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
Expand Down
9 changes: 9 additions & 0 deletions tests/testCAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading