Support more controllers than real ps4 controllers

This commit is contained in:
dec05eba
2025-10-26 14:26:46 +01:00
parent ecd9a1f13f
commit 1c24616388
4 changed files with 152 additions and 125 deletions

2
TODO
View File

@@ -218,8 +218,6 @@ Steam overlay interfers with controller input in gsr ui. Maybe move controller h
Add option to show recording status with scroll lock led (use x11 xkb). Blink when starting/stopping recording and set led on when recording is running and set led off when not recording. Add option to show recording status with scroll lock led (use x11 xkb). Blink when starting/stopping recording and set led on when recording is running and set led off when not recording.
Use /dev/input/eventN or /dev/hidrawN for controller input since /dev/input/jsN buttons are different on different controllers, for example home button is different on ps4 (10), gamesir (8) and hori (12).
For joysticks (gamepads) create a virtual device for each one (/dev/uinput) that has the same vendor, product and name. This is to make sure that it behaves the same way in applications since applications For joysticks (gamepads) create a virtual device for each one (/dev/uinput) that has the same vendor, product and name. This is to make sure that it behaves the same way in applications since applications
access joysticks directly through /dev/input/eventN or /dev/input/jsN. It needs the same number of buttons and pretend to be a controller of the same time, for example a ps4 controller access joysticks directly through /dev/input/eventN or /dev/input/jsN. It needs the same number of buttons and pretend to be a controller of the same time, for example a ps4 controller
so that games automatically display ps4 buttons if supported. so that games automatically display ps4 buttons if supported.

View File

@@ -4,8 +4,10 @@
#include "../Hotplug.hpp" #include "../Hotplug.hpp"
#include <unordered_map> #include <unordered_map>
#include <thread> #include <thread>
#include <mutex>
#include <condition_variable>
#include <poll.h> #include <poll.h>
#include <linux/joystick.h> #include <linux/input.h>
namespace gsr { namespace gsr {
static constexpr int max_js_poll_fd = 16; static constexpr int max_js_poll_fd = 16;
@@ -30,8 +32,10 @@ namespace gsr {
bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override; bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override;
void poll_events() override; void poll_events() override;
private: private:
void close_fds();
void read_events(); void read_events();
void process_js_event(int fd, js_event &event); void process_input_event(int fd, input_event &event);
void add_all_joystick_devices();
bool add_device(const char *dev_input_filepath, bool print_error = true); bool add_device(const char *dev_input_filepath, bool print_error = true);
bool remove_device(const char *dev_input_filepath); bool remove_device(const char *dev_input_filepath);
bool remove_poll_fd(int index); bool remove_poll_fd(int index);
@@ -45,6 +49,11 @@ namespace gsr {
std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id; std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id;
std::thread read_thread; std::thread read_thread;
std::thread close_fd_thread;
std::vector<int> fds_to_close;
std::mutex close_fd_mutex;
std::condition_variable close_fd_cv;
pollfd poll_fd[max_js_poll_fd]; pollfd poll_fd[max_js_poll_fd];
ExtraData extra_data[max_js_poll_fd]; ExtraData extra_data[max_js_poll_fd];
int num_poll_fd = 0; int num_poll_fd = 0;
@@ -56,8 +65,6 @@ namespace gsr {
bool down_pressed = false; bool down_pressed = false;
bool left_pressed = false; bool left_pressed = false;
bool right_pressed = false; bool right_pressed = false;
bool l3_button_pressed = false;
bool r3_button_pressed = false;
bool save_replay = false; bool save_replay = false;
bool save_1_min_replay = false; bool save_1_min_replay = false;

View File

@@ -3,92 +3,48 @@
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include <dirent.h>
#include <sys/eventfd.h> #include <sys/eventfd.h>
namespace gsr { namespace gsr {
static constexpr int button_pressed = 1; static constexpr int button_pressed = 1;
static constexpr int cross_button = 0;
static constexpr int triangle_button = 2;
static constexpr int options_button = 9;
static constexpr int playstation_button = 10;
static constexpr int l3_button = 11;
static constexpr int r3_button = 12;
static constexpr int axis_up_down = 7;
static constexpr int axis_left_right = 6;
struct DeviceId {
uint16_t vendor;
uint16_t product;
};
static bool read_file_hex_number(const char *path, unsigned int *value) {
*value = 0;
FILE *f = fopen(path, "rb");
if(!f)
return false;
fscanf(f, "%x", value);
fclose(f);
return true;
}
static DeviceId joystick_get_device_id(const char *path) {
DeviceId device_id;
device_id.vendor = 0;
device_id.product = 0;
const char *js_path_id = nullptr;
const int len = strlen(path);
for(int i = len - 1; i >= 0; --i) {
if(path[i] == '/') {
js_path_id = path + i + 1;
break;
}
}
if(!js_path_id)
return device_id;
unsigned int vendor = 0;
unsigned int product = 0;
char path_buf[1024];
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id);
if(!read_file_hex_number(path_buf, &vendor))
return device_id;
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id);
if(!read_file_hex_number(path_buf, &product))
return device_id;
device_id.vendor = vendor;
device_id.product = product;
return device_id;
}
static bool is_ps4_controller(DeviceId device_id) {
return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4);
}
static bool is_ps5_controller(DeviceId device_id) {
return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6);
}
static bool is_stadia_controller(DeviceId device_id) {
return device_id.vendor == 0x18D1 && (device_id.product == 0x9400);
}
// Returns -1 on error // Returns -1 on error
static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) { static int get_dev_input_event_id_from_filepath(const char *dev_input_filepath) {
if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0) if(strncmp(dev_input_filepath, "/dev/input/event", 16) != 0)
return -1; return -1;
int dev_input_id = -1; int dev_input_id = -1;
if(sscanf(dev_input_filepath + 13, "%d", &dev_input_id) == 1) if(sscanf(dev_input_filepath + 16, "%d", &dev_input_id) == 1)
return dev_input_id; return dev_input_id;
return -1; return -1;
} }
static inline bool supports_key(unsigned char *key_bits, unsigned int key) {
return key_bits[key/8] & (1 << (key % 8));
}
static bool supports_joystick_keys(unsigned char *key_bits) {
const int keys[7] = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_SELECT, BTN_START, BTN_SELECT };
for(int i = 0; i < 7; ++i) {
if(supports_key(key_bits, keys[i]))
return true;
}
return false;
}
static bool is_input_device_joystick(int input_fd) {
unsigned long evbit = 0;
ioctl(input_fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
if((evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY))) {
unsigned char key_bits[KEY_MAX/8 + 1];
memset(key_bits, 0, sizeof(key_bits));
ioctl(input_fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
return supports_joystick_keys(key_bits);
}
return false;
}
GlobalHotkeysJoystick::~GlobalHotkeysJoystick() { GlobalHotkeysJoystick::~GlobalHotkeysJoystick() {
if(event_fd > 0) { if(event_fd > 0) {
const uint64_t exit = 1; const uint64_t exit = 1;
@@ -98,8 +54,18 @@ namespace gsr {
if(read_thread.joinable()) if(read_thread.joinable())
read_thread.join(); read_thread.join();
if(event_fd > 0) if(event_fd > 0) {
close(event_fd); close(event_fd);
event_fd = 0;
}
close_fd_cv.notify_one();
if(close_fd_thread.joinable())
close_fd_thread.join();
for(int fd : fds_to_close) {
close(fd);
}
for(int i = 0; i < num_poll_fd; ++i) { for(int i = 0; i < num_poll_fd; ++i) {
if(poll_fd[i].fd > 0) if(poll_fd[i].fd > 0)
@@ -141,16 +107,10 @@ namespace gsr {
++num_poll_fd; ++num_poll_fd;
} }
char dev_input_path[128]; add_all_joystick_devices();
for(int i = 0; i < 8; ++i) {
snprintf(dev_input_path, sizeof(dev_input_path), "/dev/input/js%d", i);
add_device(dev_input_path, false);
}
if(num_poll_fd == 0)
fprintf(stderr, "Info: no joysticks found, assuming they might be connected later\n");
read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this); read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this);
close_fd_thread = std::thread(&GlobalHotkeysJoystick::close_fds, this);
return true; return true;
} }
@@ -214,8 +174,30 @@ namespace gsr {
} }
} }
// Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only
void GlobalHotkeysJoystick::close_fds() {
std::vector<int> current_fds_to_close;
while(event_fd > 0) {
{
std::unique_lock<std::mutex> lock(close_fd_mutex);
close_fd_cv.wait(lock, [this]{ return !fds_to_close.empty() || event_fd <= 0; });
}
{
std::lock_guard<std::mutex> lock(close_fd_mutex);
current_fds_to_close = std::move(fds_to_close);
fds_to_close.clear();
}
for(int fd : current_fds_to_close) {
close(fd);
}
current_fds_to_close.clear();
}
}
void GlobalHotkeysJoystick::read_events() { void GlobalHotkeysJoystick::read_events() {
js_event event; input_event event;
while(poll(poll_fd, num_poll_fd, -1) > 0) { while(poll(poll_fd, num_poll_fd, -1) > 0) {
for(int i = 0; i < num_poll_fd; ++i) { for(int i = 0; i < num_poll_fd; ++i) {
if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) { if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) {
@@ -223,7 +205,7 @@ namespace gsr {
goto done; goto done;
char dev_input_filepath[256]; char dev_input_filepath[256];
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/js%d", extra_data[i].dev_input_id); snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/event%d", extra_data[i].dev_input_id);
fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath); fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath);
if(remove_poll_fd(i)) if(remove_poll_fd(i))
--i; // This item was removed so we want to repeat the same index to continue to the next item --i; // This item was removed so we want to repeat the same index to continue to the next item
@@ -240,7 +222,7 @@ namespace gsr {
hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) { hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) {
switch(hotplug_action) { switch(hotplug_action) {
case HotplugAction::ADD: { case HotplugAction::ADD: {
add_device(devname); add_device(devname, false);
break; break;
} }
case HotplugAction::REMOVE: { case HotplugAction::REMOVE: {
@@ -251,7 +233,7 @@ namespace gsr {
} }
}); });
} else { } else {
process_js_event(poll_fd[i].fd, event); process_input_event(poll_fd[i].fd, event);
} }
} }
} }
@@ -260,54 +242,44 @@ namespace gsr {
; ;
} }
void GlobalHotkeysJoystick::process_js_event(int fd, js_event &event) { void GlobalHotkeysJoystick::process_input_event(int fd, input_event &event) {
if(read(fd, &event, sizeof(event)) != sizeof(event)) if(read(fd, &event, sizeof(event)) != sizeof(event))
return; return;
if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) { if(event.type == EV_KEY) {
switch(event.number) { switch(event.code) {
case playstation_button: { case BTN_MODE: {
// Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time playstation_button_pressed = (event.value == button_pressed);
playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed;
break; break;
} }
case options_button: { case BTN_START: {
if(playstation_button_pressed && event.value == button_pressed) if(playstation_button_pressed && event.value == button_pressed)
toggle_show = true; toggle_show = true;
break; break;
} }
case cross_button: { case BTN_SOUTH: {
if(playstation_button_pressed && event.value == button_pressed) if(playstation_button_pressed && event.value == button_pressed)
save_1_min_replay = true; save_1_min_replay = true;
break; break;
} }
case triangle_button: { case BTN_NORTH: {
if(playstation_button_pressed && event.value == button_pressed) if(playstation_button_pressed && event.value == button_pressed)
save_10_min_replay = true; save_10_min_replay = true;
break; break;
} }
case l3_button: {
l3_button_pressed = event.value == button_pressed;
break;
}
case r3_button: {
r3_button_pressed = event.value == button_pressed;
break;
}
} }
} else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) { } else if(event.type == EV_ABS && playstation_button_pressed) {
const int trigger_threshold = 16383;
const bool prev_up_pressed = up_pressed; const bool prev_up_pressed = up_pressed;
const bool prev_down_pressed = down_pressed; const bool prev_down_pressed = down_pressed;
const bool prev_left_pressed = left_pressed; const bool prev_left_pressed = left_pressed;
const bool prev_right_pressed = right_pressed; const bool prev_right_pressed = right_pressed;
if(event.number == axis_up_down) { if(event.code == ABS_HAT0Y) {
up_pressed = event.value <= -trigger_threshold; up_pressed = event.value == -1;
down_pressed = event.value >= trigger_threshold; down_pressed = event.value == 1;
} else if(event.number == axis_left_right) { } else if(event.code == ABS_HAT0X) {
left_pressed = event.value <= -trigger_threshold; left_pressed = event.value == -1;
right_pressed = event.value >= trigger_threshold; right_pressed = event.value == 1;
} }
if(up_pressed && !prev_up_pressed) if(up_pressed && !prev_up_pressed)
@@ -321,13 +293,36 @@ namespace gsr {
} }
} }
void GlobalHotkeysJoystick::add_all_joystick_devices() {
DIR *dir = opendir("/dev/input");
if(!dir) {
fprintf(stderr, "Error: failed to open /dev/input, error: %s\n", strerror(errno));
return;
}
char dev_input_filepath[1024];
for(;;) {
struct dirent *entry = readdir(dir);
if(!entry)
break;
if(strncmp(entry->d_name, "event", 5) != 0)
continue;
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/%s", entry->d_name);
add_device(dev_input_filepath, false);
}
closedir(dir);
}
bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) { bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) {
if(num_poll_fd >= max_js_poll_fd) { if(num_poll_fd >= max_js_poll_fd) {
fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath); fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath);
return false; return false;
} }
const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
if(dev_input_id == -1) if(dev_input_id == -1)
return false; return false;
@@ -338,6 +333,15 @@ namespace gsr {
return false; return false;
} }
if(!is_input_device_joystick(fd)) {
{
std::lock_guard<std::mutex> lock(close_fd_mutex);
fds_to_close.push_back(fd);
}
close_fd_cv.notify_one();
return false;
}
poll_fd[num_poll_fd] = { poll_fd[num_poll_fd] = {
fd, fd,
POLLIN, POLLIN,
@@ -356,7 +360,7 @@ namespace gsr {
} }
bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) { bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) {
const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
if(dev_input_id == -1) if(dev_input_id == -1)
return false; return false;
@@ -372,8 +376,13 @@ namespace gsr {
if(index < 0 || index >= num_poll_fd) if(index < 0 || index >= num_poll_fd)
return false; return false;
if(poll_fd[index].fd > 0) if(poll_fd[index].fd > 0) {
close(poll_fd[index].fd); {
std::lock_guard<std::mutex> lock(close_fd_mutex);
fds_to_close.push_back(poll_fd[index].fd);
}
close_fd_cv.notify_one();
}
for(int i = index + 1; i < num_poll_fd; ++i) { for(int i = index + 1; i < num_poll_fd; ++i) {
poll_fd[i - 1] = poll_fd[i]; poll_fd[i - 1] = poll_fd[i];

View File

@@ -234,14 +234,23 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
/* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */ /* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */
static void* keyboard_event_close_fds_callback(void *userdata) { static void* keyboard_event_close_fds_callback(void *userdata) {
keyboard_event *self = userdata; keyboard_event *self = userdata;
int fds_to_close_now[MAX_CLOSE_FDS];
int num_fds_to_close_now = 0;
while(self->running) { while(self->running) {
pthread_mutex_lock(&self->close_dev_input_mutex); pthread_mutex_lock(&self->close_dev_input_mutex);
for(int i = 0; i < self->num_close_fds; ++i) { for(int i = 0; i < self->num_close_fds; ++i) {
close(self->close_fds[i]); fds_to_close_now[i] = self->close_fds[i];
} }
num_fds_to_close_now = self->num_close_fds;
self->num_close_fds = 0; self->num_close_fds = 0;
pthread_mutex_unlock(&self->close_dev_input_mutex); pthread_mutex_unlock(&self->close_dev_input_mutex);
for(int i = 0; i < num_fds_to_close_now; ++i) {
close(fds_to_close_now[i]);
}
num_fds_to_close_now = 0;
usleep(100 * 1000); /* 100 milliseconds */ usleep(100 * 1000); /* 100 milliseconds */
} }
return NULL; return NULL;
@@ -456,9 +465,13 @@ static void keyboard_event_remove_event(keyboard_event *self, int index) {
if(index < 0 || index >= self->num_event_polls) if(index < 0 || index >= self->num_event_polls)
return; return;
if(self->event_polls[index].fd > 0) { const int poll_fd = self->event_polls[index].fd;
ioctl(self->event_polls[index].fd, EVIOCGRAB, 0); if(poll_fd > 0) {
close(self->event_polls[index].fd); ioctl(poll_fd, EVIOCGRAB, 0);
if(!keyboard_event_try_add_close_fd(self, poll_fd)) {
fprintf(stderr, "Error: failed to add immediately, closing now\n");
close(poll_fd);
}
} }
free(self->event_extra_data[index].key_states); free(self->event_extra_data[index].key_states);
free(self->event_extra_data[index].key_presses_grabbed); free(self->event_extra_data[index].key_presses_grabbed);