Add support for SDL3 joystick input driver

Made possible by EIREXE, xsellier and the SDL team.

This commit includes statically linked SDL3 for Windows, Linux and macOS.
The vendored copy of SDL3 was setup to only build the required subsystems
for gamepad/joystick support, with some patches to be able to make it as
minimal as possible and reduce the impact on binary size and code size.

Co-authored-by: Álex Román Núñez <eirexe123@gmail.com>
Co-authored-by: Xavier Sellier <xsellier@gmail.com>
Co-authored-by: Rémi Verschelde <rverschelde@gmail.com>
This commit is contained in:
Nintorch
2025-05-28 14:22:20 +05:00
committed by Rémi Verschelde
parent 987832be46
commit 0b3496fb4f
330 changed files with 154930 additions and 1561 deletions

View File

@ -8,7 +8,6 @@ import platform_linuxbsd_builders
common_linuxbsd = [
"crash_handler_linuxbsd.cpp",
"os_linuxbsd.cpp",
"joypad_linux.cpp",
"freedesktop_portal_desktop.cpp",
"freedesktop_screensaver.cpp",
"freedesktop_at_spi_monitor.cpp",

View File

@ -394,6 +394,18 @@ def configure(env: "SConsEnvironment"):
else:
env["udev"] = False # Linux specific
if env["sdl"]:
if env["builtin_sdl"]:
env.Append(CPPDEFINES=["SDL_ENABLED"])
elif os.system("pkg-config --exists fontconfig") == 0: # 0 means found
env.ParseConfig("pkg-config sdl3 --cflags --libs")
env.Append(CPPDEFINES=["SDL_ENABLED"])
else:
print_warning(
"SDL3 development libraries not found, and `builtin_sdl` was explicitly disabled. Disabling SDL input driver support."
)
env["sdl"] = False
# Linkflags below this line should typically stay the last ones
if not env["builtin_zlib"]:
env.ParseConfig("pkg-config zlib --cflags --libs")

View File

@ -1,613 +0,0 @@
/**************************************************************************/
/* joypad_linux.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifdef JOYDEV_ENABLED
#include "joypad_linux.h"
#include "core/os/os.h"
#include <dirent.h>
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#include <cerrno>
#ifdef UDEV_ENABLED
#ifdef SOWRAP_ENABLED
#include "libudev-so_wrap.h"
#else
#include <libudev.h>
#endif
#endif
#define LONG_BITS (sizeof(long) * 8)
#define test_bit(nr, addr) (((1UL << ((nr) % LONG_BITS)) & ((addr)[(nr) / LONG_BITS])) != 0)
#define NBITS(x) ((((x) - 1) / LONG_BITS) + 1)
#ifdef UDEV_ENABLED
static const char *ignore_str = "/dev/input/js";
#endif
// On Linux with Steam Input Xbox 360 devices have an index appended to their device name, this index is
// the Steam Input gamepad index
#define VALVE_GAMEPAD_NAME_PREFIX "Microsoft X-Box 360 pad "
// IDs used by Steam Input virtual controllers.
// See https://partner.steamgames.com/doc/features/steam_controller/steam_input_gamepad_emulation_bestpractices
#define VALVE_GAMEPAD_VID 0x28DE
#define VALVE_GAMEPAD_PID 0x11FF
JoypadLinux::Joypad::~Joypad() {
for (int i = 0; i < MAX_ABS; i++) {
if (abs_info[i]) {
memdelete(abs_info[i]);
}
}
}
void JoypadLinux::Joypad::reset() {
dpad.clear();
fd = -1;
for (int i = 0; i < MAX_ABS; i++) {
abs_map[i] = -1;
curr_axis[i] = 0;
}
events.clear();
}
JoypadLinux::JoypadLinux(Input *in) {
#ifdef UDEV_ENABLED
if (OS::get_singleton()->is_sandboxed()) {
// Linux binaries in sandboxes / containers need special handling because
// libudev doesn't work there. So we need to fallback to manual parsing
// of /dev/input in such case.
use_udev = false;
print_verbose("JoypadLinux: udev enabled, but detected incompatible sandboxed mode. Falling back to /dev/input to detect joypads.");
}
#ifdef SOWRAP_ENABLED
else {
#ifdef DEBUG_ENABLED
int dylibloader_verbose = 1;
#else
int dylibloader_verbose = 0;
#endif
use_udev = initialize_libudev(dylibloader_verbose) == 0;
if (use_udev) {
if (!udev_new || !udev_unref || !udev_enumerate_new || !udev_enumerate_add_match_subsystem || !udev_enumerate_scan_devices || !udev_enumerate_get_list_entry || !udev_list_entry_get_next || !udev_list_entry_get_name || !udev_device_new_from_syspath || !udev_device_get_devnode || !udev_device_get_action || !udev_device_unref || !udev_enumerate_unref || !udev_monitor_new_from_netlink || !udev_monitor_filter_add_match_subsystem_devtype || !udev_monitor_enable_receiving || !udev_monitor_get_fd || !udev_monitor_receive_device || !udev_monitor_unref) {
// There's no API to check version, check if functions are available instead.
use_udev = false;
print_verbose("JoypadLinux: Unsupported udev library version!");
} else {
print_verbose("JoypadLinux: udev enabled and loaded successfully.");
}
} else {
print_verbose("JoypadLinux: udev enabled, but couldn't be loaded. Falling back to /dev/input to detect joypads.");
}
}
#endif // SOWRAP_ENABLED
#else
print_verbose("JoypadLinux: udev disabled, parsing /dev/input to detect joypads.");
#endif // UDEV_ENABLED
input = in;
monitor_joypads_thread.start(monitor_joypads_thread_func, this);
joypad_events_thread.start(joypad_events_thread_func, this);
}
JoypadLinux::~JoypadLinux() {
monitor_joypads_exit.set();
joypad_events_exit.set();
monitor_joypads_thread.wait_to_finish();
joypad_events_thread.wait_to_finish();
close_joypads();
}
void JoypadLinux::monitor_joypads_thread_func(void *p_user) {
if (p_user) {
JoypadLinux *joy = static_cast<JoypadLinux *>(p_user);
joy->monitor_joypads_thread_run();
}
}
void JoypadLinux::monitor_joypads_thread_run() {
#ifdef UDEV_ENABLED
if (use_udev) {
udev *_udev = udev_new();
if (!_udev) {
use_udev = false;
ERR_PRINT("Failed getting an udev context, falling back to parsing /dev/input.");
monitor_joypads();
} else {
enumerate_joypads(_udev);
monitor_joypads(_udev);
udev_unref(_udev);
}
} else {
monitor_joypads();
}
#else
monitor_joypads();
#endif
}
#ifdef UDEV_ENABLED
void JoypadLinux::enumerate_joypads(udev *p_udev) {
udev_enumerate *enumerate;
udev_list_entry *devices, *dev_list_entry;
udev_device *dev;
enumerate = udev_enumerate_new(p_udev);
udev_enumerate_add_match_subsystem(enumerate, "input");
udev_enumerate_scan_devices(enumerate);
devices = udev_enumerate_get_list_entry(enumerate);
udev_list_entry_foreach(dev_list_entry, devices) {
const char *path = udev_list_entry_get_name(dev_list_entry);
dev = udev_device_new_from_syspath(p_udev, path);
const char *devnode = udev_device_get_devnode(dev);
if (devnode) {
String devnode_str = devnode;
if (!devnode_str.contains(ignore_str)) {
open_joypad(devnode);
}
}
udev_device_unref(dev);
}
udev_enumerate_unref(enumerate);
}
void JoypadLinux::monitor_joypads(udev *p_udev) {
udev_device *dev = nullptr;
udev_monitor *mon = udev_monitor_new_from_netlink(p_udev, "udev");
udev_monitor_filter_add_match_subsystem_devtype(mon, "input", nullptr);
udev_monitor_enable_receiving(mon);
int fd = udev_monitor_get_fd(mon);
while (!monitor_joypads_exit.is_set()) {
fd_set fds;
struct timeval tv;
int ret;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = 0;
tv.tv_usec = 0;
ret = select(fd + 1, &fds, nullptr, nullptr, &tv);
/* Check if our file descriptor has received data. */
if (ret > 0 && FD_ISSET(fd, &fds)) {
/* Make the call to receive the device.
select() ensured that this will not block. */
dev = udev_monitor_receive_device(mon);
if (dev && udev_device_get_devnode(dev) != nullptr) {
String action = udev_device_get_action(dev);
const char *devnode = udev_device_get_devnode(dev);
if (devnode) {
String devnode_str = devnode;
if (!devnode_str.contains(ignore_str)) {
if (action == "add") {
open_joypad(devnode);
} else if (String(action) == "remove") {
close_joypad(devnode);
}
}
}
udev_device_unref(dev);
}
}
OS::get_singleton()->delay_usec(50'000);
}
udev_monitor_unref(mon);
}
#endif
void JoypadLinux::monitor_joypads() {
while (!monitor_joypads_exit.is_set()) {
DIR *input_directory;
input_directory = opendir("/dev/input");
if (input_directory) {
struct dirent *current;
char fname[64];
while ((current = readdir(input_directory)) != nullptr) {
if (strncmp(current->d_name, "event", 5) != 0) {
continue;
}
sprintf(fname, "/dev/input/%.*s", 16, current->d_name);
if (!attached_devices.has(fname)) {
open_joypad(fname);
}
}
}
closedir(input_directory);
OS::get_singleton()->delay_usec(1'000'000);
}
}
void JoypadLinux::close_joypads() {
for (int i = 0; i < JOYPADS_MAX; i++) {
MutexLock lock(joypads_mutex[i]);
Joypad &joypad = joypads[i];
close_joypad(joypad, i);
}
}
void JoypadLinux::close_joypad(const char *p_devpath) {
for (int i = 0; i < JOYPADS_MAX; i++) {
MutexLock lock(joypads_mutex[i]);
Joypad &joypad = joypads[i];
if (joypads[i].devpath == p_devpath) {
close_joypad(joypad, i);
}
}
}
void JoypadLinux::close_joypad(Joypad &p_joypad, int p_id) {
if (p_joypad.fd != -1) {
close(p_joypad.fd);
p_joypad.fd = -1;
attached_devices.erase(p_joypad.devpath);
input->joy_connection_changed(p_id, false, "");
}
p_joypad.events.clear();
}
static String _hex_str(uint8_t p_byte) {
static const char *dict = "0123456789abcdef";
char ret[3];
ret[2] = 0;
ret[0] = dict[p_byte >> 4];
ret[1] = dict[p_byte & 0xF];
return ret;
}
void JoypadLinux::setup_joypad_properties(Joypad &p_joypad) {
unsigned long keybit[NBITS(KEY_MAX)] = { 0 };
unsigned long absbit[NBITS(ABS_MAX)] = { 0 };
int num_buttons = 0;
int num_axes = 0;
if ((ioctl(p_joypad.fd, EVIOCGBIT(EV_KEY, sizeof(keybit)), keybit) < 0) ||
(ioctl(p_joypad.fd, EVIOCGBIT(EV_ABS, sizeof(absbit)), absbit) < 0)) {
return;
}
for (int i = BTN_JOYSTICK; i < KEY_MAX; ++i) {
if (test_bit(i, keybit)) {
p_joypad.key_map[i] = num_buttons++;
}
}
for (int i = BTN_MISC; i < BTN_JOYSTICK; ++i) {
if (test_bit(i, keybit)) {
p_joypad.key_map[i] = num_buttons++;
}
}
for (int i = 0; i < ABS_MISC; ++i) {
/* Skip hats */
if (i == ABS_HAT0X) {
i = ABS_HAT3Y;
continue;
}
if (test_bit(i, absbit)) {
p_joypad.abs_map[i] = num_axes++;
p_joypad.abs_info[i] = memnew(input_absinfo);
if (ioctl(p_joypad.fd, EVIOCGABS(i), p_joypad.abs_info[i]) < 0) {
memdelete(p_joypad.abs_info[i]);
p_joypad.abs_info[i] = nullptr;
}
}
}
p_joypad.force_feedback = false;
p_joypad.ff_effect_timestamp = 0;
unsigned long ffbit[NBITS(FF_CNT)];
if (ioctl(p_joypad.fd, EVIOCGBIT(EV_FF, sizeof(ffbit)), ffbit) != -1) {
if (test_bit(FF_RUMBLE, ffbit)) {
p_joypad.force_feedback = true;
}
}
}
void JoypadLinux::open_joypad(const char *p_path) {
int joy_num = input->get_unused_joy_id();
int fd = open(p_path, O_RDWR | O_NONBLOCK);
if (fd != -1 && joy_num != -1) {
unsigned long evbit[NBITS(EV_MAX)] = { 0 };
unsigned long keybit[NBITS(KEY_MAX)] = { 0 };
unsigned long absbit[NBITS(ABS_MAX)] = { 0 };
// add to attached devices so we don't try to open it again
attached_devices.push_back(String(p_path));
if ((ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), evbit) < 0) ||
(ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keybit)), keybit) < 0) ||
(ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absbit)), absbit) < 0)) {
close(fd);
return;
}
// Check if the device supports basic gamepad events
bool has_abs_left = (test_bit(ABS_X, absbit) && test_bit(ABS_Y, absbit));
bool has_abs_right = (test_bit(ABS_RX, absbit) && test_bit(ABS_RY, absbit));
if (!(test_bit(EV_KEY, evbit) && test_bit(EV_ABS, evbit) && (has_abs_left || has_abs_right))) {
close(fd);
return;
}
char uid[128];
char namebuf[128];
String name = "";
input_id inpid;
if (ioctl(fd, EVIOCGNAME(sizeof(namebuf)), namebuf) >= 0) {
name = namebuf;
}
for (const String &word : name.to_lower().split(" ")) {
if (banned_words.has(word)) {
return;
}
}
if (ioctl(fd, EVIOCGID, &inpid) < 0) {
close(fd);
return;
}
uint16_t vendor = BSWAP16(inpid.vendor);
uint16_t product = BSWAP16(inpid.product);
uint16_t version = BSWAP16(inpid.version);
if (input->should_ignore_device(vendor, product)) {
// This can be true in cases where Steam is passing information into the game to ignore
// original gamepads when using virtual rebindings (See SteamInput).
return;
}
MutexLock lock(joypads_mutex[joy_num]);
Joypad &joypad = joypads[joy_num];
joypad.reset();
joypad.fd = fd;
joypad.devpath = String(p_path);
setup_joypad_properties(joypad);
sprintf(uid, "%04x%04x", BSWAP16(inpid.bustype), 0);
if (inpid.vendor && inpid.product && inpid.version) {
Dictionary joypad_info;
joypad_info["vendor_id"] = inpid.vendor;
joypad_info["product_id"] = inpid.product;
joypad_info["raw_name"] = name;
sprintf(uid + String(uid).length(), "%04x%04x%04x%04x%04x%04x", vendor, 0, product, 0, version, 0);
if (inpid.vendor == VALVE_GAMEPAD_VID && inpid.product == VALVE_GAMEPAD_PID) {
if (name.begins_with(VALVE_GAMEPAD_NAME_PREFIX)) {
String idx_str = name.substr(strlen(VALVE_GAMEPAD_NAME_PREFIX));
if (idx_str.is_valid_int()) {
joypad_info["steam_input_index"] = idx_str.to_int();
}
}
}
input->joy_connection_changed(joy_num, true, name, uid, joypad_info);
} else {
String uidname = uid;
int uidlen = MIN(name.length(), 11);
for (int i = 0; i < uidlen; i++) {
uidname = uidname + _hex_str(name[i]);
}
uidname += "00";
input->joy_connection_changed(joy_num, true, name, uidname);
}
}
}
void JoypadLinux::joypad_vibration_start(Joypad &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) {
if (!p_joypad.force_feedback || p_joypad.fd == -1 || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) {
return;
}
if (p_joypad.ff_effect_id != -1) {
joypad_vibration_stop(p_joypad, p_timestamp);
}
struct ff_effect effect;
effect.type = FF_RUMBLE;
effect.id = -1;
effect.u.rumble.weak_magnitude = std::floor(p_weak_magnitude * (float)0xffff);
effect.u.rumble.strong_magnitude = std::floor(p_strong_magnitude * (float)0xffff);
effect.replay.length = std::floor(p_duration * 1000);
effect.replay.delay = 0;
if (ioctl(p_joypad.fd, EVIOCSFF, &effect) < 0) {
return;
}
struct input_event play;
play.type = EV_FF;
play.code = effect.id;
play.value = 1;
if (write(p_joypad.fd, (const void *)&play, sizeof(play)) == -1) {
print_verbose("Couldn't write to Joypad device.");
}
p_joypad.ff_effect_id = effect.id;
p_joypad.ff_effect_timestamp = p_timestamp;
}
void JoypadLinux::joypad_vibration_stop(Joypad &p_joypad, uint64_t p_timestamp) {
if (!p_joypad.force_feedback || p_joypad.fd == -1 || p_joypad.ff_effect_id == -1) {
return;
}
if (ioctl(p_joypad.fd, EVIOCRMFF, p_joypad.ff_effect_id) < 0) {
return;
}
p_joypad.ff_effect_id = -1;
p_joypad.ff_effect_timestamp = p_timestamp;
}
float JoypadLinux::axis_correct(const input_absinfo *p_abs, int p_value) const {
int min = p_abs->minimum;
int max = p_abs->maximum;
// Convert to a value between -1.0f and 1.0f.
return 2.0f * (p_value - min) / (max - min) - 1.0f;
}
void JoypadLinux::joypad_events_thread_func(void *p_user) {
if (p_user) {
JoypadLinux *joy = (JoypadLinux *)p_user;
joy->joypad_events_thread_run();
}
}
void JoypadLinux::joypad_events_thread_run() {
while (!joypad_events_exit.is_set()) {
bool no_events = true;
for (int i = 0; i < JOYPADS_MAX; i++) {
MutexLock lock(joypads_mutex[i]);
Joypad &joypad = joypads[i];
if (joypad.fd == -1) {
continue;
}
input_event event;
while (read(joypad.fd, &event, sizeof(event)) > 0) {
no_events = false;
JoypadEvent joypad_event;
joypad_event.type = event.type;
joypad_event.code = event.code;
joypad_event.value = event.value;
joypad.events.push_back(joypad_event);
}
if (errno != EAGAIN) {
close_joypad(joypad, i);
}
}
if (no_events) {
OS::get_singleton()->delay_usec(10'000);
}
}
}
void JoypadLinux::process_joypads() {
for (int i = 0; i < JOYPADS_MAX; i++) {
MutexLock lock(joypads_mutex[i]);
Joypad &joypad = joypads[i];
if (joypad.fd == -1) {
continue;
}
for (uint32_t j = 0; j < joypad.events.size(); j++) {
const JoypadEvent &joypad_event = joypad.events[j];
// joypad_event may be tainted and out of MAX_KEY range, which will cause
// joypad.key_map[joypad_event.code] to crash
if (joypad_event.code >= MAX_KEY) {
return;
}
switch (joypad_event.type) {
case EV_KEY:
input->joy_button(i, (JoyButton)joypad.key_map[joypad_event.code], joypad_event.value);
break;
case EV_ABS:
switch (joypad_event.code) {
case ABS_HAT0X:
if (joypad_event.value != 0) {
if (joypad_event.value < 0) {
joypad.dpad.set_flag(HatMask::LEFT);
joypad.dpad.clear_flag(HatMask::RIGHT);
} else {
joypad.dpad.set_flag(HatMask::RIGHT);
joypad.dpad.clear_flag(HatMask::LEFT);
}
} else {
joypad.dpad.clear_flag(HatMask::LEFT);
joypad.dpad.clear_flag(HatMask::RIGHT);
}
input->joy_hat(i, joypad.dpad);
break;
case ABS_HAT0Y:
if (joypad_event.value != 0) {
if (joypad_event.value < 0) {
joypad.dpad.set_flag(HatMask::UP);
joypad.dpad.clear_flag(HatMask::DOWN);
} else {
joypad.dpad.set_flag(HatMask::DOWN);
joypad.dpad.clear_flag(HatMask::UP);
}
} else {
joypad.dpad.clear_flag(HatMask::UP);
joypad.dpad.clear_flag(HatMask::DOWN);
}
input->joy_hat(i, joypad.dpad);
break;
default:
if (joypad_event.code >= MAX_ABS) {
return;
}
if (joypad.abs_map[joypad_event.code] != -1 && joypad.abs_info[joypad_event.code]) {
float value = axis_correct(joypad.abs_info[joypad_event.code], joypad_event.value);
joypad.curr_axis[joypad.abs_map[joypad_event.code]] = value;
}
break;
}
break;
}
}
joypad.events.clear();
for (int j = 0; j < MAX_ABS; j++) {
int index = joypad.abs_map[j];
if (index != -1) {
input->joy_axis(i, (JoyAxis)index, joypad.curr_axis[index]);
}
}
if (joypad.force_feedback) {
uint64_t timestamp = input->get_joy_vibration_timestamp(i);
if (timestamp > joypad.ff_effect_timestamp) {
Vector2 strength = input->get_joy_vibration_strength(i);
float duration = input->get_joy_vibration_duration(i);
if (strength.x == 0 && strength.y == 0) {
joypad_vibration_stop(joypad, timestamp);
} else {
joypad_vibration_start(joypad, strength.x, strength.y, duration, timestamp);
}
}
}
}
}
#endif // JOYDEV_ENABLED

View File

@ -1,136 +0,0 @@
/**************************************************************************/
/* joypad_linux.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#ifdef JOYDEV_ENABLED
#include "core/input/input.h"
#include "core/os/mutex.h"
#include "core/os/thread.h"
#include "core/templates/local_vector.h"
struct input_absinfo;
class JoypadLinux {
public:
JoypadLinux(Input *in);
~JoypadLinux();
void process_joypads();
private:
enum {
JOYPADS_MAX = 16,
MAX_ABS = 63,
MAX_KEY = 767, // Hack because <linux/input.h> can't be included here
};
struct JoypadEvent {
uint16_t type;
uint16_t code;
int32_t value;
};
struct Joypad {
float curr_axis[MAX_ABS];
int key_map[MAX_KEY];
int abs_map[MAX_ABS];
BitField<HatMask> dpad = HatMask::CENTER;
int fd = -1;
String devpath;
input_absinfo *abs_info[MAX_ABS] = {};
bool force_feedback = false;
int ff_effect_id = 0;
uint64_t ff_effect_timestamp = 0;
LocalVector<JoypadEvent> events;
~Joypad();
void reset();
};
#ifdef UDEV_ENABLED
bool use_udev = false;
#endif
Input *input = nullptr;
SafeFlag monitor_joypads_exit;
SafeFlag joypad_events_exit;
Thread monitor_joypads_thread;
Thread joypad_events_thread;
Joypad joypads[JOYPADS_MAX];
Mutex joypads_mutex[JOYPADS_MAX];
Vector<String> attached_devices;
// List of lowercase words that will prevent the controller from being recognized if its name matches.
// This is done to prevent trackpads, graphics tablets and motherboard LED controllers from being
// recognized as controllers (and taking up controller ID slots as a result).
// Only whole words are matched within the controller name string. The match is case-insensitive.
const Vector<String> banned_words = {
"touchpad", // Matches e.g. "SynPS/2 Synaptics TouchPad", "Sony Interactive Entertainment DualSense Wireless Controller Touchpad"
"trackpad",
"clickpad",
"keyboard", // Matches e.g. "PG-90215 Keyboard", "Usb Keyboard Usb Keyboard Consumer Control"
"mouse", // Matches e.g. "Mouse passthrough"
"pen", // Matches e.g. "Wacom One by Wacom S Pen"
"finger", // Matches e.g. "Wacom HID 495F Finger"
"led", // Matches e.g. "ASRock LED Controller"
};
static void monitor_joypads_thread_func(void *p_user);
void monitor_joypads_thread_run();
void open_joypad(const char *p_path);
void setup_joypad_properties(Joypad &p_joypad);
void close_joypads();
void close_joypad(const char *p_devpath);
void close_joypad(Joypad &p_joypad, int p_id);
#ifdef UDEV_ENABLED
void enumerate_joypads(struct udev *p_udev);
void monitor_joypads(struct udev *p_udev);
#endif
void monitor_joypads();
void joypad_vibration_start(Joypad &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp);
void joypad_vibration_stop(Joypad &p_joypad, uint64_t p_timestamp);
static void joypad_events_thread_func(void *p_user);
void joypad_events_thread_run();
float axis_correct(const input_absinfo *p_abs, int p_value) const;
};
#endif // JOYDEV_ENABLED

View File

@ -32,6 +32,9 @@
#include "core/io/certs_compressed.gen.h"
#include "core/io/dir_access.h"
#ifdef SDL_ENABLED
#include "drivers/sdl/joypad_sdl.h"
#endif
#include "main/main.h"
#include "servers/display_server.h"
#include "servers/rendering_server.h"
@ -158,8 +161,13 @@ void OS_LinuxBSD::initialize() {
}
void OS_LinuxBSD::initialize_joypads() {
#ifdef JOYDEV_ENABLED
joypad = memnew(JoypadLinux(Input::get_singleton()));
#ifdef SDL_ENABLED
joypad_sdl = memnew(JoypadSDL());
if (joypad_sdl->initialize() != OK) {
ERR_PRINT("Couldn't initialize SDL joypad input driver.");
memdelete(joypad_sdl);
joypad_sdl = nullptr;
}
#endif
}
@ -241,9 +249,9 @@ void OS_LinuxBSD::finalize() {
driver_alsamidi.close();
#endif
#ifdef JOYDEV_ENABLED
if (joypad) {
memdelete(joypad);
#ifdef SDL_ENABLED
if (joypad_sdl) {
memdelete(joypad_sdl);
}
#endif
}
@ -973,8 +981,10 @@ void OS_LinuxBSD::run() {
while (true) {
DisplayServer::get_singleton()->process_events(); // get rid of pending events
#ifdef JOYDEV_ENABLED
joypad->process_joypads();
#ifdef SDL_ENABLED
if (joypad_sdl) {
joypad_sdl->process_events();
}
#endif
if (Main::iteration()) {
break;

View File

@ -31,7 +31,6 @@
#pragma once
#include "crash_handler_linuxbsd.h"
#include "joypad_linux.h"
#include "core/input/input.h"
#include "drivers/alsa/audio_driver_alsa.h"
@ -48,6 +47,8 @@
#endif
#endif
class JoypadSDL;
class OS_LinuxBSD : public OS_Unix {
virtual void delete_main_loop() override;
@ -60,8 +61,8 @@ class OS_LinuxBSD : public OS_Unix {
int _stretch_to_fc(int p_stretch) const;
#endif
#ifdef JOYDEV_ENABLED
JoypadLinux *joypad = nullptr;
#ifdef SDL_ENABLED
JoypadSDL *joypad_sdl = nullptr;
#endif
#ifdef ALSA_ENABLED

View File

@ -32,8 +32,6 @@
#ifdef X11_ENABLED
#include "joypad_linux.h"
#include "core/input/input.h"
#include "core/os/mutex.h"
#include "core/os/thread.h"

View File

@ -197,6 +197,10 @@ def configure(env: "SConsEnvironment"):
if env["builtin_libtheora"] and env["arch"] == "x86_64":
env["x86_libtheora_opt_gcc"] = True
if env["sdl"]:
env.Append(CPPDEFINES=["SDL_ENABLED"])
env.Append(LINKFLAGS=["-framework", "ForceFeedback"])
## Flags
env.Prepend(CPPPATH=["#platform/macos"])

View File

@ -33,12 +33,13 @@
#include "crash_handler_macos.h"
#include "core/input/input.h"
#import "drivers/apple/joypad_apple.h"
#import "drivers/coreaudio/audio_driver_coreaudio.h"
#import "drivers/coremidi/midi_driver_coremidi.h"
#include "drivers/unix/os_unix.h"
#include "servers/audio_server.h"
class JoypadSDL;
class OS_MacOS : public OS_Unix {
#ifdef COREAUDIO_ENABLED
AudioDriverCoreAudio audio_driver;
@ -62,7 +63,9 @@ protected:
int argc = 0;
char **argv = nullptr;
JoypadApple *joypad_apple = nullptr;
#ifdef SDL_ENABLED
JoypadSDL *joypad_sdl = nullptr;
#endif
MainLoop *main_loop = nullptr;
CFRunLoopTimerRef wait_timer = nil;

View File

@ -43,6 +43,10 @@
#include "drivers/apple/os_log_logger.h"
#include "main/main.h"
#ifdef SDL_ENABLED
#include "drivers/sdl/joypad_sdl.h"
#endif
#include <dlfcn.h>
#include <libproc.h>
#import <mach-o/dyld.h>
@ -230,13 +234,22 @@ void OS_MacOS::finalize() {
delete_main_loop();
if (joypad_apple) {
memdelete(joypad_apple);
#ifdef SDL_ENABLED
if (joypad_sdl) {
memdelete(joypad_sdl);
}
#endif
}
void OS_MacOS::initialize_joypads() {
joypad_apple = memnew(JoypadApple());
#ifdef SDL_ENABLED
joypad_sdl = memnew(JoypadSDL());
if (joypad_sdl->initialize() != OK) {
ERR_PRINT("Couldn't initialize SDL joypad input driver.");
memdelete(joypad_sdl);
joypad_sdl = nullptr;
}
#endif
}
void OS_MacOS::set_main_loop(MainLoop *p_main_loop) {
@ -1078,7 +1091,11 @@ void OS_MacOS_NSApp::start_main() {
} else if (ds) {
ds->process_events();
}
joypad_apple->process_joypads();
#ifdef SDL_ENABLED
if (joypad_sdl) {
joypad_sdl->process_events();
}
#endif
if (Main::iteration() || sig_received) {
terminate();

View File

@ -17,7 +17,6 @@ common_win = [
"os_windows.cpp",
"display_server_windows.cpp",
"key_mapping_windows.cpp",
"joypad_windows.cpp",
"tts_windows.cpp",
"windows_terminal_logger.cpp",
"windows_utils.cpp",

View File

@ -480,6 +480,9 @@ def configure_msvc(env: "SConsEnvironment"):
if not env["use_volk"]:
LIBS += ["vulkan"]
if env["sdl"]:
env.Append(CPPDEFINES=["SDL_ENABLED"])
if env["d3d12"]:
check_d3d12_installed(env, env["arch"] + "-msvc")
@ -867,6 +870,9 @@ def configure_mingw(env: "SConsEnvironment"):
if not env["use_volk"]:
env.Append(LIBS=["vulkan"])
if env["sdl"]:
env.Append(CPPDEFINES=["SDL_ENABLED"])
if env["d3d12"]:
if env["use_llvm"]:
check_d3d12_installed(env, env["arch"] + "-llvm")

View File

@ -43,6 +43,10 @@
#include "main/main.h"
#include "scene/resources/texture.h"
#ifdef SDL_ENABLED
#include "drivers/sdl/joypad_sdl.h"
#endif
#include "servers/rendering/dummy/rasterizer_dummy.h"
#if defined(VULKAN_ENABLED)
@ -3771,7 +3775,11 @@ void DisplayServerWindows::process_events() {
ERR_FAIL_COND(!Thread::is_main_thread());
if (!drop_events) {
joypad->process_joypads();
#ifdef SDL_ENABLED
if (joypad_sdl) {
joypad_sdl->process_events();
}
#endif
}
_THREAD_SAFE_LOCK_
@ -5962,9 +5970,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
}
} break;
case WM_DEVICECHANGE: {
joypad->probe_joypads();
} break;
case WM_DESTROY: {
#ifdef ACCESSKIT_ENABLED
if (accessibility_driver) {
@ -7155,7 +7160,16 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
ERR_FAIL_MSG("Failed to create main window.");
}
joypad = new JoypadWindows(&windows[MAIN_WINDOW_ID].hWnd);
#ifdef SDL_ENABLED
joypad_sdl = memnew(JoypadSDL());
if (joypad_sdl->initialize() == OK) {
joypad_sdl->setup_sdl_helper_window(windows[MAIN_WINDOW_ID].hWnd);
} else {
ERR_PRINT("Couldn't initialize SDL joypad input driver.");
memdelete(joypad_sdl);
joypad_sdl = nullptr;
}
#endif
for (int i = 0; i < WINDOW_FLAG_MAX; i++) {
if (p_flags & (1 << i)) {
@ -7309,7 +7323,11 @@ DisplayServerWindows::~DisplayServerWindows() {
E->erase();
}
delete joypad;
#ifdef SDL_ENABLED
if (joypad_sdl) {
memdelete(joypad_sdl);
}
#endif
touch_state.clear();
cursors_cache.clear();

View File

@ -31,7 +31,6 @@
#pragma once
#include "crash_handler_windows.h"
#include "joypad_windows.h"
#include "key_mapping_windows.h"
#include "tts_windows.h"
@ -196,6 +195,8 @@ class DropTargetWindows;
#define WDA_EXCLUDEFROMCAPTURE 0x00000011
#endif
class JoypadSDL;
class DisplayServerWindows : public DisplayServer {
GDSOFTCLASS(DisplayServerWindows, DisplayServer);
@ -375,7 +376,9 @@ class DisplayServerWindows : public DisplayServer {
HWND parent_hwnd = 0;
};
JoypadWindows *joypad = nullptr;
#ifdef SDL_ENABLED
JoypadSDL *joypad_sdl = nullptr;
#endif
HHOOK mouse_monitor = nullptr;
List<WindowID> popup_list;
uint64_t time_since_popup = 0;

View File

@ -35,6 +35,7 @@
#include "core/os/time.h"
#include <fileapi.h>
#include <shellapi.h>
// Helpers

View File

@ -1,625 +0,0 @@
/**************************************************************************/
/* joypad_windows.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "joypad_windows.h"
#include <oleauto.h>
#include <wbemidl.h>
DWORD WINAPI _xinput_get_state(DWORD dwUserIndex, XINPUT_STATE *pState) {
return ERROR_DEVICE_NOT_CONNECTED;
}
DWORD WINAPI _xinput_set_state(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration) {
return ERROR_DEVICE_NOT_CONNECTED;
}
MMRESULT WINAPI _winmm_get_joycaps(UINT uJoyID, LPJOYCAPSW pjc, UINT cbjc) {
return MMSYSERR_NODRIVER;
}
JoypadWindows::JoypadWindows() {
}
JoypadWindows::JoypadWindows(HWND *hwnd) {
input = Input::get_singleton();
hWnd = hwnd;
x_joypad_probe_count = 0;
d_joypad_count = 0;
dinput = nullptr;
xinput_dll = nullptr;
xinput_get_state = nullptr;
xinput_set_state = nullptr;
winmm_get_joycaps = nullptr;
load_xinput();
for (int i = 0; i < JOYPADS_MAX; i++) {
attached_joypads[i] = false;
}
HRESULT result = DirectInput8Create(GetModuleHandle(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8, (void **)&dinput, nullptr);
if (result == DI_OK) {
probe_joypads();
} else {
ERR_PRINT("Couldn't initialize DirectInput. Error: " + itos(result));
if (result == DIERR_OUTOFMEMORY) {
ERR_PRINT("The Windows DirectInput subsystem could not allocate sufficient memory.");
ERR_PRINT("Rebooting your PC may solve this issue.");
}
// Ensure dinput is still a nullptr.
dinput = nullptr;
}
}
JoypadWindows::~JoypadWindows() {
close_d_joypad();
if (dinput) {
dinput->Release();
}
unload_winmm();
unload_xinput();
}
bool JoypadWindows::is_d_joypad_known(const GUID &p_guid) {
for (int i = 0; i < JOYPADS_MAX; i++) {
if (d_joypads[i].guid == p_guid) {
d_joypads[i].confirmed = true;
return true;
}
}
return false;
}
// adapted from SDL2, works a lot better than the MSDN version
bool JoypadWindows::is_xinput_joypad(const GUID *p_guid) {
static GUID IID_ValveStreamingGamepad = { MAKELONG(0x28DE, 0x11FF), 0x28DE, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_X360WiredGamepad = { MAKELONG(0x045E, 0x02A1), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_X360WirelessGamepad = { MAKELONG(0x045E, 0x028E), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XSWirelessGamepad = { MAKELONG(0x045E, 0x0B13), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XEliteWirelessGamepad = { MAKELONG(0x045E, 0x0B05), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XOneWiredGamepad = { MAKELONG(0x045E, 0x02FF), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XOneWirelessGamepad = { MAKELONG(0x045E, 0x02DD), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XOneNewWirelessGamepad = { MAKELONG(0x045E, 0x02D1), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XOneSWirelessGamepad = { MAKELONG(0x045E, 0x02EA), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XOneSBluetoothGamepad = { MAKELONG(0x045E, 0x02E0), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XOneEliteWirelessGamepad = { MAKELONG(0x045E, 0x02E3), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
static GUID IID_XOneElite2WirelessGamepad = { MAKELONG(0x045E, 0x0B22), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
if (memcmp(p_guid, &IID_ValveStreamingGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_X360WiredGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_X360WirelessGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XSWirelessGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XEliteWirelessGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XOneWiredGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XOneWirelessGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XOneNewWirelessGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XOneSWirelessGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XOneSBluetoothGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XOneEliteWirelessGamepad, sizeof(*p_guid)) == 0 ||
memcmp(p_guid, &IID_XOneElite2WirelessGamepad, sizeof(*p_guid)) == 0) {
return true;
}
PRAWINPUTDEVICELIST dev_list = nullptr;
unsigned int dev_list_count = 0;
if (GetRawInputDeviceList(nullptr, &dev_list_count, sizeof(RAWINPUTDEVICELIST)) == (UINT)-1) {
return false;
}
dev_list = (PRAWINPUTDEVICELIST)memalloc(sizeof(RAWINPUTDEVICELIST) * dev_list_count);
ERR_FAIL_NULL_V_MSG(dev_list, false, "Out of memory.");
if (GetRawInputDeviceList(dev_list, &dev_list_count, sizeof(RAWINPUTDEVICELIST)) == (UINT)-1) {
memfree(dev_list);
return false;
}
for (unsigned int i = 0; i < dev_list_count; i++) {
RID_DEVICE_INFO rdi;
char dev_name[128];
UINT rdiSize = sizeof(rdi);
UINT nameSize = sizeof(dev_name);
rdi.cbSize = rdiSize;
if ((dev_list[i].dwType == RIM_TYPEHID) &&
(GetRawInputDeviceInfoA(dev_list[i].hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) != (UINT)-1) &&
(MAKELONG(rdi.hid.dwVendorId, rdi.hid.dwProductId) == (LONG)p_guid->Data1) &&
(GetRawInputDeviceInfoA(dev_list[i].hDevice, RIDI_DEVICENAME, &dev_name, &nameSize) != (UINT)-1) &&
(strstr(dev_name, "IG_") != nullptr)) {
memfree(dev_list);
return true;
}
}
memfree(dev_list);
return false;
}
void JoypadWindows::probe_xinput_joypad(const String &name) {
if (x_joypad_probe_count >= XUSER_MAX_COUNT) {
return;
}
int i = x_joypad_probe_count;
x_joypad_probe_count++;
ZeroMemory(&x_joypads[i].state, sizeof(XINPUT_STATE));
DWORD dwResult = xinput_get_state(i, &x_joypads[i].state);
if (dwResult == ERROR_SUCCESS) {
int id = input->get_unused_joy_id();
if (id != -1 && !x_joypads[i].attached) {
x_joypads[i].attached = true;
x_joypads[i].id = id;
x_joypads[i].ff_timestamp = 0;
x_joypads[i].ff_end_timestamp = 0;
x_joypads[i].vibrating = false;
attached_joypads[id] = true;
Dictionary joypad_info;
String joypad_name;
joypad_info["xinput_index"] = (int)i;
JOYCAPSW jc;
memset(&jc, 0, sizeof(JOYCAPSW));
MMRESULT jcResult = winmm_get_joycaps((UINT)id, &jc, sizeof(JOYCAPSW));
if (jcResult == JOYERR_NOERROR) {
joypad_info["vendor_id"] = itos(jc.wMid);
joypad_info["product_id"] = itos(jc.wPid);
if (!name.is_empty()) {
joypad_name = name.trim_prefix("Controller (").trim_suffix(")");
}
}
input->joy_connection_changed(id, true, joypad_name, "__XINPUT_DEVICE__", joypad_info);
}
} else if (x_joypads[i].attached) {
x_joypads[i].attached = false;
attached_joypads[x_joypads[i].id] = false;
input->joy_connection_changed(x_joypads[i].id, false, "");
}
}
bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) {
ERR_FAIL_NULL_V_MSG(dinput, false, "DirectInput not initialized. Rebooting your PC may solve this issue.");
HRESULT hr;
int num = input->get_unused_joy_id();
if (is_d_joypad_known(instance->guidInstance) || num == -1) {
return false;
}
d_joypads[num] = dinput_gamepad();
dinput_gamepad *joy = &d_joypads[num];
const DWORD devtype = (instance->dwDevType & 0xFF);
if ((devtype != DI8DEVTYPE_JOYSTICK) && (devtype != DI8DEVTYPE_GAMEPAD) && (devtype != DI8DEVTYPE_1STPERSON) && (devtype != DI8DEVTYPE_DRIVING)) {
return false;
}
hr = dinput->CreateDevice(instance->guidInstance, &joy->di_joy, nullptr);
if (FAILED(hr)) {
return false;
}
const GUID &guid = instance->guidProduct;
char uid[128];
ERR_FAIL_COND_V_MSG(memcmp(&guid.Data4[2], "PIDVID", 6), false, "DirectInput device not recognized.");
WORD type = BSWAP16(0x03);
WORD vendor = BSWAP16(LOWORD(guid.Data1));
WORD product = BSWAP16(HIWORD(guid.Data1));
WORD version = 0;
sprintf_s(uid, "%04x%04x%04x%04x%04x%04x%04x%04x", type, 0, vendor, 0, product, 0, version, 0);
Dictionary joypad_info;
joypad_info["vendor_id"] = itos(vendor);
joypad_info["product_id"] = itos(product);
id_to_change = num;
slider_count = 0;
joy->di_joy->SetDataFormat(&c_dfDIJoystick2);
joy->di_joy->SetCooperativeLevel(*hWnd, DISCL_FOREGROUND);
joy->di_joy->EnumObjects(objectsCallback, this, 0);
joy->joy_axis.sort();
joy->guid = instance->guidInstance;
const String &name = String(instance->tszProductName).trim_prefix("Controller (").trim_suffix(")");
input->joy_connection_changed(num, true, name, uid, joypad_info);
joy->attached = true;
joy->id = num;
attached_joypads[num] = true;
joy->confirmed = true;
d_joypad_count++;
return true;
}
void JoypadWindows::setup_d_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_joy_id) {
if (ob->dwType & DIDFT_AXIS) {
HRESULT res;
DIPROPRANGE prop_range;
DIPROPDWORD dilong;
LONG ofs;
if (ob->guidType == GUID_XAxis) {
ofs = DIJOFS_X;
} else if (ob->guidType == GUID_YAxis) {
ofs = DIJOFS_Y;
} else if (ob->guidType == GUID_ZAxis) {
ofs = DIJOFS_Z;
} else if (ob->guidType == GUID_RxAxis) {
ofs = DIJOFS_RX;
} else if (ob->guidType == GUID_RyAxis) {
ofs = DIJOFS_RY;
} else if (ob->guidType == GUID_RzAxis) {
ofs = DIJOFS_RZ;
} else if (ob->guidType == GUID_Slider) {
if (slider_count < 2) {
ofs = DIJOFS_SLIDER(slider_count);
slider_count++;
} else {
return;
}
} else {
return;
}
prop_range.diph.dwSize = sizeof(DIPROPRANGE);
prop_range.diph.dwHeaderSize = sizeof(DIPROPHEADER);
prop_range.diph.dwObj = ob->dwType;
prop_range.diph.dwHow = DIPH_BYID;
prop_range.lMin = -MAX_JOY_AXIS;
prop_range.lMax = +MAX_JOY_AXIS;
dinput_gamepad &joy = d_joypads[p_joy_id];
res = IDirectInputDevice8_SetProperty(joy.di_joy, DIPROP_RANGE, &prop_range.diph);
if (FAILED(res)) {
return;
}
dilong.diph.dwSize = sizeof(dilong);
dilong.diph.dwHeaderSize = sizeof(dilong.diph);
dilong.diph.dwObj = ob->dwType;
dilong.diph.dwHow = DIPH_BYID;
dilong.dwData = 0;
res = IDirectInputDevice8_SetProperty(joy.di_joy, DIPROP_DEADZONE, &dilong.diph);
if (FAILED(res)) {
return;
}
joy.joy_axis.push_back(ofs);
}
}
BOOL CALLBACK JoypadWindows::enumCallback(const DIDEVICEINSTANCE *p_instance, void *p_context) {
JoypadWindows *self = static_cast<JoypadWindows *>(p_context);
if (self->is_xinput_joypad(&p_instance->guidProduct)) {
self->probe_xinput_joypad(p_instance->tszProductName);
return DIENUM_CONTINUE;
}
self->setup_dinput_joypad(p_instance);
return DIENUM_CONTINUE;
}
BOOL CALLBACK JoypadWindows::objectsCallback(const DIDEVICEOBJECTINSTANCE *p_instance, void *p_context) {
JoypadWindows *self = static_cast<JoypadWindows *>(p_context);
self->setup_d_joypad_object(p_instance, self->id_to_change);
return DIENUM_CONTINUE;
}
void JoypadWindows::close_d_joypad(int id) {
if (id == -1) {
for (int i = 0; i < JOYPADS_MAX; i++) {
close_d_joypad(i);
}
return;
}
if (!d_joypads[id].attached) {
return;
}
d_joypads[id].di_joy->Unacquire();
d_joypads[id].di_joy->Release();
d_joypads[id].attached = false;
attached_joypads[d_joypads[id].id] = false;
d_joypads[id].guid.Data1 = d_joypads[id].guid.Data2 = d_joypads[id].guid.Data3 = 0;
input->joy_connection_changed(d_joypads[id].id, false, "");
d_joypad_count--;
}
void JoypadWindows::probe_joypads() {
ERR_FAIL_NULL_MSG(dinput, "DirectInput not initialized. Rebooting your PC may solve this issue.");
for (int i = 0; i < d_joypad_count; i++) {
d_joypads[i].confirmed = false; // Flag DirectInput devices for re-checking their availability.
}
x_joypad_probe_count = 0;
// Probe _all attached_ joypad devices.
dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, enumCallback, this, DIEDFL_ATTACHEDONLY);
for (int i = x_joypad_probe_count; i < XUSER_MAX_COUNT; i++) {
// Handle disconnect of XInput devices.
// And act as a fallback, just in case DirectInput could not find the device.
probe_xinput_joypad();
}
for (int i = 0; i < d_joypad_count; i++) {
if (!d_joypads[i].confirmed) {
close_d_joypad(i); // Any DirectInput device not found during probing is considered as disconnected.
}
}
}
void JoypadWindows::process_joypads() {
HRESULT hr;
// Handle XInput joypads.
for (int i = 0; i < XUSER_MAX_COUNT; i++) {
xinput_gamepad &joy = x_joypads[i];
if (!joy.attached) {
continue;
}
ZeroMemory(&joy.state, sizeof(XINPUT_STATE));
xinput_get_state(i, &joy.state);
if (joy.state.dwPacketNumber != joy.last_packet) {
int button_mask = XINPUT_GAMEPAD_DPAD_UP;
for (int j = 0; j <= 16; j++) {
input->joy_button(joy.id, (JoyButton)j, joy.state.Gamepad.wButtons & button_mask);
button_mask = button_mask * 2;
}
input->joy_axis(joy.id, JoyAxis::LEFT_X, axis_correct(joy.state.Gamepad.sThumbLX, true));
input->joy_axis(joy.id, JoyAxis::LEFT_Y, axis_correct(joy.state.Gamepad.sThumbLY, true, false, true));
input->joy_axis(joy.id, JoyAxis::RIGHT_X, axis_correct(joy.state.Gamepad.sThumbRX, true));
input->joy_axis(joy.id, JoyAxis::RIGHT_Y, axis_correct(joy.state.Gamepad.sThumbRY, true, false, true));
input->joy_axis(joy.id, JoyAxis::TRIGGER_LEFT, axis_correct(joy.state.Gamepad.bLeftTrigger, true, true));
input->joy_axis(joy.id, JoyAxis::TRIGGER_RIGHT, axis_correct(joy.state.Gamepad.bRightTrigger, true, true));
joy.last_packet = joy.state.dwPacketNumber;
}
uint64_t timestamp = input->get_joy_vibration_timestamp(joy.id);
if (timestamp > joy.ff_timestamp) {
Vector2 strength = input->get_joy_vibration_strength(joy.id);
float duration = input->get_joy_vibration_duration(joy.id);
if (strength.x == 0 && strength.y == 0) {
joypad_vibration_stop_xinput(i, timestamp);
} else {
joypad_vibration_start_xinput(i, strength.x, strength.y, duration, timestamp);
}
} else if (joy.vibrating && joy.ff_end_timestamp != 0) {
uint64_t current_time = OS::get_singleton()->get_ticks_usec();
if (current_time >= joy.ff_end_timestamp) {
joypad_vibration_stop_xinput(i, current_time);
}
}
}
// Handle DirectIndput joypads.
for (int i = 0; i < JOYPADS_MAX; i++) {
dinput_gamepad *joy = &d_joypads[i];
if (!joy->attached) {
continue;
}
DIJOYSTATE2 js;
hr = joy->di_joy->Poll();
if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED) {
IDirectInputDevice8_Acquire(joy->di_joy);
joy->di_joy->Poll();
}
hr = joy->di_joy->GetDeviceState(sizeof(DIJOYSTATE2), &js);
if (FAILED(hr)) {
continue;
}
post_hat(joy->id, js.rgdwPOV[0]);
for (int j = 0; j < 128; j++) {
if (js.rgbButtons[j] & 0x80) {
if (!joy->last_buttons[j]) {
input->joy_button(joy->id, (JoyButton)j, true);
joy->last_buttons[j] = true;
}
} else {
if (joy->last_buttons[j]) {
input->joy_button(joy->id, (JoyButton)j, false);
joy->last_buttons[j] = false;
}
}
}
// on mingw, these constants are not constants
int count = 8;
const LONG axes[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ, (LONG)DIJOFS_SLIDER(0), (LONG)DIJOFS_SLIDER(1) };
int values[] = { js.lX, js.lY, js.lZ, js.lRx, js.lRy, js.lRz, js.rglSlider[0], js.rglSlider[1] };
for (uint32_t j = 0; j < joy->joy_axis.size(); j++) {
for (int k = 0; k < count; k++) {
if (joy->joy_axis[j] == axes[k]) {
input->joy_axis(joy->id, (JoyAxis)j, axis_correct(values[k]));
break;
}
}
}
}
return;
}
void JoypadWindows::post_hat(int p_device, DWORD p_dpad) {
BitField<HatMask> dpad_val = HatMask::CENTER;
// Should be -1 when centered, but according to docs:
// "Some drivers report the centered position of the POV indicator as 65,535. Determine whether the indicator is centered as follows:
// BOOL POVCentered = (LOWORD(dwPOV) == 0xFFFF);"
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee416628(v%3Dvs.85)#remarks
if (LOWORD(p_dpad) == 0xFFFF) {
// Do nothing.
// dpad_val.set_flag(HatMask::CENTER);
}
if (p_dpad == 0) {
dpad_val.set_flag(HatMask::UP);
} else if (p_dpad == 4500) {
dpad_val.set_flag(HatMask::UP);
dpad_val.set_flag(HatMask::RIGHT);
} else if (p_dpad == 9000) {
dpad_val.set_flag(HatMask::RIGHT);
} else if (p_dpad == 13500) {
dpad_val.set_flag(HatMask::RIGHT);
dpad_val.set_flag(HatMask::DOWN);
} else if (p_dpad == 18000) {
dpad_val.set_flag(HatMask::DOWN);
} else if (p_dpad == 22500) {
dpad_val.set_flag(HatMask::DOWN);
dpad_val.set_flag(HatMask::LEFT);
} else if (p_dpad == 27000) {
dpad_val.set_flag(HatMask::LEFT);
} else if (p_dpad == 31500) {
dpad_val.set_flag(HatMask::LEFT);
dpad_val.set_flag(HatMask::UP);
}
input->joy_hat(p_device, dpad_val);
}
float JoypadWindows::axis_correct(int p_val, bool p_xinput, bool p_trigger, bool p_negate) const {
if (Math::abs(p_val) < MIN_JOY_AXIS) {
return p_trigger ? -1.0f : 0.0f;
}
if (!p_xinput) {
return p_val / (float)MAX_JOY_AXIS;
}
if (p_trigger) {
// Convert to a value between -1.0f and 1.0f.
return 2.0f * p_val / (float)MAX_TRIGGER - 1.0f;
}
float value;
if (p_val < 0) {
value = p_val / (float)MAX_JOY_AXIS;
} else {
value = p_val / (float)(MAX_JOY_AXIS - 1);
}
if (p_negate) {
value = -value;
}
return value;
}
void JoypadWindows::joypad_vibration_start_xinput(int p_device, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) {
xinput_gamepad &joy = x_joypads[p_device];
if (joy.attached) {
XINPUT_VIBRATION effect;
effect.wLeftMotorSpeed = (65535 * p_strong_magnitude);
effect.wRightMotorSpeed = (65535 * p_weak_magnitude);
if (xinput_set_state(p_device, &effect) == ERROR_SUCCESS) {
joy.ff_timestamp = p_timestamp;
joy.ff_end_timestamp = p_duration == 0 ? 0 : p_timestamp + (uint64_t)(p_duration * 1000000.0);
joy.vibrating = true;
}
}
}
void JoypadWindows::joypad_vibration_stop_xinput(int p_device, uint64_t p_timestamp) {
xinput_gamepad &joy = x_joypads[p_device];
if (joy.attached) {
XINPUT_VIBRATION effect;
effect.wLeftMotorSpeed = 0;
effect.wRightMotorSpeed = 0;
if (xinput_set_state(p_device, &effect) == ERROR_SUCCESS) {
joy.ff_timestamp = p_timestamp;
joy.vibrating = false;
}
}
}
void JoypadWindows::load_xinput() {
xinput_get_state = &_xinput_get_state;
xinput_set_state = &_xinput_set_state;
winmm_get_joycaps = &_winmm_get_joycaps;
bool legacy_xinput = false;
xinput_dll = LoadLibrary("XInput1_4.dll");
if (!xinput_dll) {
xinput_dll = LoadLibrary("XInput1_3.dll");
if (!xinput_dll) {
xinput_dll = LoadLibrary("XInput9_1_0.dll");
legacy_xinput = true;
}
}
if (!xinput_dll) {
print_verbose("Could not find XInput, using DirectInput only");
return;
}
// (LPCSTR)100 is the magic number to get XInputGetStateEx, which also provides the state for the guide button
LPCSTR get_state_func_name = legacy_xinput ? "XInputGetState" : (LPCSTR)100;
XInputGetState_t func = (XInputGetState_t)(void *)GetProcAddress((HMODULE)xinput_dll, get_state_func_name);
XInputSetState_t set_func = (XInputSetState_t)(void *)GetProcAddress((HMODULE)xinput_dll, "XInputSetState");
if (!func || !set_func) {
unload_xinput();
return;
}
xinput_get_state = func;
xinput_set_state = set_func;
winmm_dll = LoadLibrary("Winmm.dll");
if (winmm_dll) {
joyGetDevCaps_t caps_func = (joyGetDevCaps_t)(void *)GetProcAddress((HMODULE)winmm_dll, "joyGetDevCapsW");
if (caps_func) {
winmm_get_joycaps = caps_func;
} else {
unload_winmm();
}
}
}
void JoypadWindows::unload_xinput() {
if (xinput_dll) {
FreeLibrary((HMODULE)xinput_dll);
}
}
void JoypadWindows::unload_winmm() {
if (winmm_dll) {
FreeLibrary((HMODULE)winmm_dll);
}
}

View File

@ -1,149 +0,0 @@
/**************************************************************************/
/* joypad_windows.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "os_windows.h"
#define DIRECTINPUT_VERSION 0x0800
#include <dinput.h>
#include <xinput.h>
#include <mmsystem.h>
#ifndef SAFE_RELEASE // when Windows Media Device M? is not present
#define SAFE_RELEASE(x) \
if (x != nullptr) { \
x->Release(); \
x = nullptr; \
}
#endif
#ifndef XUSER_MAX_COUNT
#define XUSER_MAX_COUNT 4
#endif
class JoypadWindows {
public:
JoypadWindows();
JoypadWindows(HWND *hwnd);
~JoypadWindows();
void probe_joypads();
void process_joypads();
private:
enum {
JOYPADS_MAX = 16,
JOY_AXIS_COUNT = 6,
MIN_JOY_AXIS = 10,
MAX_JOY_AXIS = 32768,
MAX_JOY_BUTTONS = 128,
KEY_EVENT_BUFFER_SIZE = 512,
MAX_TRIGGER = 255
};
struct dinput_gamepad {
int id;
bool attached;
bool confirmed;
bool last_buttons[MAX_JOY_BUTTONS];
DWORD last_pad;
LPDIRECTINPUTDEVICE8 di_joy;
LocalVector<LONG> joy_axis;
GUID guid;
dinput_gamepad() {
id = -1;
last_pad = -1;
attached = false;
confirmed = false;
di_joy = nullptr;
guid = {};
for (int i = 0; i < MAX_JOY_BUTTONS; i++) {
last_buttons[i] = false;
}
}
};
struct xinput_gamepad {
int id = 0;
bool attached = false;
bool vibrating = false;
DWORD last_packet = 0;
XINPUT_STATE state;
uint64_t ff_timestamp = 0;
uint64_t ff_end_timestamp = 0;
};
typedef DWORD(WINAPI *XInputGetState_t)(DWORD dwUserIndex, XINPUT_STATE *pState);
typedef DWORD(WINAPI *XInputSetState_t)(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration);
typedef MMRESULT(WINAPI *joyGetDevCaps_t)(UINT uJoyID, LPJOYCAPSW pjc, UINT cbjc);
HWND *hWnd = nullptr;
HANDLE xinput_dll;
HANDLE winmm_dll;
LPDIRECTINPUT8 dinput;
Input *input = nullptr;
int id_to_change;
int slider_count;
int x_joypad_probe_count; // XInput equivalent to dinput_gamepad.confirmed.
int d_joypad_count;
bool attached_joypads[JOYPADS_MAX];
dinput_gamepad d_joypads[JOYPADS_MAX];
xinput_gamepad x_joypads[XUSER_MAX_COUNT];
static BOOL CALLBACK enumCallback(const DIDEVICEINSTANCE *p_instance, void *p_context);
static BOOL CALLBACK objectsCallback(const DIDEVICEOBJECTINSTANCE *instance, void *context);
void setup_d_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_joy_id);
void close_d_joypad(int id = -1);
void load_xinput();
void unload_xinput();
void unload_winmm();
void post_hat(int p_device, DWORD p_dpad);
bool is_d_joypad_known(const GUID &p_guid);
bool is_xinput_joypad(const GUID *p_guid);
bool setup_dinput_joypad(const DIDEVICEINSTANCE *instance);
void probe_xinput_joypad(const String &name = ""); // Handles connect, disconnect & re-connect for XInput joypads.
void joypad_vibration_start_xinput(int p_device, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp);
void joypad_vibration_stop_xinput(int p_device, uint64_t p_timestamp);
float axis_correct(int p_val, bool p_xinput = false, bool p_trigger = false, bool p_negate = false) const;
XInputGetState_t xinput_get_state;
XInputSetState_t xinput_set_state;
joyGetDevCaps_t winmm_get_joycaps; // Only for reading info on XInput joypads.
};

View File

@ -31,7 +31,6 @@
#include "os_windows.h"
#include "display_server_windows.h"
#include "joypad_windows.h"
#include "lang_table.h"
#include "windows_terminal_logger.h"
#include "windows_utils.h"

View File

@ -67,6 +67,14 @@
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x4
#endif
#ifndef SAFE_RELEASE // when Windows Media Device M? is not present
#define SAFE_RELEASE(x) \
if (x != nullptr) { \
x->Release(); \
x = nullptr; \
}
#endif
template <typename T>
class ComAutoreleaseRef {
public:
@ -90,8 +98,6 @@ public:
}
};
class JoypadWindows;
class OS_Windows : public OS {
uint64_t target_ticks = 0;
uint64_t ticks_start = 0;