fix: support input from multiple gamepads #263

Merged
Boria138 merged 1 commits from sdl-fall into main 2026-09-14 07:54:41 +00:00
6 changed files with 183 additions and 41 deletions

No files matched your search

+1
View File
@@ -41,6 +41,7 @@
### Fixed
- Исправлено управление интерфейсом при одновременном подключении нескольких геймпадов
- Исправлена коллизия пользовательских данных автоустановок с одинаковыми именами исходных исполняемых файлов
- Исправлен поиск скриптов и локалей через `XDG_DATA_DIRS` в NixOS
- Исправлена проверка и установка недостающих i586-зависимостей в ALT Linux перед запуском игр
Binary file not shown.
+136 -35
View File
@@ -3,28 +3,118 @@
#include <stdint.h>
#include <stdlib.h>
#define AXIS_ACTIVATION_THRESHOLD 16000
typedef struct {
SDL_Gamepad *gamepad;
SDL_JoystickID instance_id;
int sdl_type;
bool buttons[SDL_GAMEPAD_BUTTON_COUNT];
bool axes_active[SDL_GAMEPAD_AXIS_COUNT];
} GamepadEntry;
typedef struct {
GamepadEntry *entries;
int count;
SDL_JoystickID active_instance_id;
} PortProtonGamepad;
static PortProtonGamepad *open_gamepad(SDL_JoystickID instance_id)
static bool has_gamepad(const PortProtonGamepad *gamepads,
SDL_JoystickID instance_id)
{
for (int index = 0; index < gamepads->count; index++) {
if (gamepads->entries[index].instance_id == instance_id) {
return true;
}
}
return false;
}
static bool add_gamepad(PortProtonGamepad *gamepads,
SDL_JoystickID instance_id)
{
SDL_Gamepad *gamepad = SDL_OpenGamepad(instance_id);
if (gamepad == NULL) {
return NULL;
return false;
}
PortProtonGamepad *result = malloc(sizeof(*result));
if (result == NULL) {
GamepadEntry *entries = realloc(
gamepads->entries, sizeof(*entries) * (gamepads->count + 1));
if (entries == NULL) {
SDL_CloseGamepad(gamepad);
SDL_SetError("Failed to allocate gamepad handle");
return SDL_SetError("Failed to allocate gamepad handle");
}
gamepads->entries = entries;
GamepadEntry entry = {0};
entry.gamepad = gamepad;
entry.instance_id = instance_id;
entry.sdl_type = SDL_GetGamepadTypeForID(instance_id);
entries[gamepads->count] = entry;
gamepads->count++;
return true;
}
static void remove_disconnected(PortProtonGamepad *gamepads)
{
int active_count = 0;
for (int index = 0; index < gamepads->count; index++) {
GamepadEntry entry = gamepads->entries[index];
if (SDL_GamepadConnected(entry.gamepad)) {
gamepads->entries[active_count++] = entry;
} else {
if (gamepads->active_instance_id == entry.instance_id) {
gamepads->active_instance_id = 0;
}
SDL_CloseGamepad(entry.gamepad);
}
}
gamepads->count = active_count;
}
static void update_gamepads(PortProtonGamepad *gamepads)
{
SDL_UpdateGamepads();
remove_disconnected(gamepads);
int count = 0;
SDL_JoystickID *instance_ids = SDL_GetGamepads(&count);
if (instance_ids == NULL) {
return;
}
for (int index = 0; index < count; index++) {
if (!has_gamepad(gamepads, instance_ids[index])) {
add_gamepad(gamepads, instance_ids[index]);
}
}
SDL_free(instance_ids);
}
static bool update_entry_activity(GamepadEntry *entry)
{
bool activated = false;
for (int button = 0; button < SDL_GAMEPAD_BUTTON_COUNT; button++) {
bool pressed = SDL_GetGamepadButton(entry->gamepad, button);
activated |= pressed && !entry->buttons[button];
entry->buttons[button] = pressed;
}
for (int axis = 0; axis < SDL_GAMEPAD_AXIS_COUNT; axis++) {
int value = SDL_GetGamepadAxis(entry->gamepad, axis);
bool active = abs(value) >= AXIS_ACTIVATION_THRESHOLD;
activated |= active && !entry->axes_active[axis];
entry->axes_active[axis] = active;
}
return activated;
}
static const GamepadEntry *get_active_gamepad(const PortProtonGamepad *gamepads)
{
if (gamepads == NULL) {
return NULL;
}
result->gamepad = gamepad;
result->instance_id = instance_id;
result->sdl_type = SDL_GetGamepadTypeForID(instance_id);
return result;
for (int index = 0; index < gamepads->count; index++) {
if (gamepads->entries[index].instance_id == gamepads->active_instance_id) {
return &gamepads->entries[index];
}
}
return NULL;
}
PortProtonGamepad *portproton_gamepad_find(void)
@@ -33,23 +123,17 @@ PortProtonGamepad *portproton_gamepad_find(void)
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
return NULL;
}
SDL_PumpEvents();
SDL_UpdateGamepads();
int count = 0;
SDL_JoystickID *gamepads = SDL_GetGamepads(&count);
if (gamepads == NULL) {
PortProtonGamepad *result = calloc(1, sizeof(*result));
if (result == NULL) {
SDL_SetError("Failed to allocate gamepad collection");
return NULL;
}
if (count == 0) {
SDL_free(gamepads);
update_gamepads(result);
if (result->count == 0) {
free(result);
SDL_ClearError();
return NULL;
}
PortProtonGamepad *result = NULL;
for (int index = 0; index < count && result == NULL; index++) {
result = open_gamepad(gamepads[index]);
}
SDL_free(gamepads);
return result;
}
@@ -64,56 +148,73 @@ void portproton_gamepad_close(PortProtonGamepad *gamepad)
if (gamepad == NULL) {
return;
}
if (gamepad->gamepad != NULL) {
SDL_CloseGamepad(gamepad->gamepad);
for (int index = 0; index < gamepad->count; index++) {
SDL_CloseGamepad(gamepad->entries[index].gamepad);
}
free(gamepad->entries);
free(gamepad);
}
bool portproton_gamepad_connected(const PortProtonGamepad *gamepad)
{
return gamepad != NULL && gamepad->gamepad != NULL &&
SDL_GamepadConnected(gamepad->gamepad);
return gamepad != NULL && gamepad->count > 0;
}
void portproton_gamepad_update(void)
void portproton_gamepad_update(PortProtonGamepad *gamepad)
{
SDL_UpdateGamepads();
if (gamepad != NULL) {
update_gamepads(gamepad);
for (int index = 0; index < gamepad->count; index++) {
if (update_entry_activity(&gamepad->entries[index])) {
gamepad->active_instance_id = gamepad->entries[index].instance_id;
}
}
}
}
int portproton_gamepad_get_button(const PortProtonGamepad *gamepad, int button)
{
if (gamepad == NULL || gamepad->gamepad == NULL) {
const GamepadEntry *active = get_active_gamepad(gamepad);
if (active == NULL) {
return 0;
}
return SDL_GetGamepadButton(gamepad->gamepad, button);
return SDL_GetGamepadButton(active->gamepad, button);
}
int16_t portproton_gamepad_get_axis(const PortProtonGamepad *gamepad, int axis)
{
if (gamepad == NULL || gamepad->gamepad == NULL) {
const GamepadEntry *active = get_active_gamepad(gamepad);
if (active == NULL) {
return 0;
}
return SDL_GetGamepadAxis(gamepad->gamepad, axis);
return SDL_GetGamepadAxis(active->gamepad, axis);
}
uint32_t portproton_gamepad_get_active_instance_id(
const PortProtonGamepad *gamepad)
{
return gamepad != NULL ? gamepad->active_instance_id : 0;
}
const char *portproton_gamepad_get_name(const PortProtonGamepad *gamepad)
{
if (gamepad == NULL || gamepad->gamepad == NULL) {
if (gamepad == NULL || gamepad->count == 0) {
return "";
}
const char *name = SDL_GetGamepadName(gamepad->gamepad);
const char *name = SDL_GetGamepadName(gamepad->entries[0].gamepad);
return name != NULL ? name : "";
}
uint32_t portproton_gamepad_get_instance_id(const PortProtonGamepad *gamepad)
{
return gamepad != NULL ? gamepad->instance_id : 0;
return gamepad != NULL && gamepad->count > 0
? gamepad->entries[0].instance_id : 0;
}
int portproton_gamepad_get_type(const PortProtonGamepad *gamepad)
{
return gamepad != NULL ? gamepad->sdl_type : 0;
return gamepad != NULL && gamepad->count > 0
? gamepad->entries[0].sdl_type : 0;
}
void portproton_gamepad_shutdown(void)
+8 -2
View File
@@ -175,7 +175,7 @@ class GamepadRuntimeMixin(InputMixin):
keyboard.update_keyboard()
def find_gamepad(self) -> SDLGamepad | None:
"""Find the first SDL3 gamepad with a standardized mapping."""
"""Open the SDL3 handle that combines all connected gamepads."""
try:
gamepad = find_gamepad()
self._last_gamepad_error = None
@@ -353,10 +353,16 @@ class GamepadRuntimeMixin(InputMixin):
self._last_gamepad_check_time = current_time
return
try:
active_gamepad.update()
active_changed = active_gamepad.update()
if not active_gamepad.connected():
self.gamepad_hotplug.emit('remove')
return
if active_changed:
self._reset_input_state()
logger.info(
"Active gamepad changed to SDL instance %s",
active_gamepad.active_instance_id,
)
self._poll_button_events(active_gamepad, current_time)
self._handle_pending_menu_fullscreen(current_time)
self._poll_hat_events(active_gamepad, current_time)
+14 -4
View File
@@ -28,12 +28,14 @@ _library.portproton_gamepad_close.argtypes = [_handle]
_library.portproton_gamepad_close.restype = None
_library.portproton_gamepad_connected.argtypes = [_handle]
_library.portproton_gamepad_connected.restype = ctypes.c_bool
_library.portproton_gamepad_update.argtypes = []
_library.portproton_gamepad_update.argtypes = [_handle]
_library.portproton_gamepad_update.restype = None
_library.portproton_gamepad_get_button.argtypes = [_handle, ctypes.c_int]
_library.portproton_gamepad_get_button.restype = ctypes.c_int
_library.portproton_gamepad_get_axis.argtypes = [_handle, ctypes.c_int]
_library.portproton_gamepad_get_axis.restype = ctypes.c_int16
_library.portproton_gamepad_get_active_instance_id.argtypes = [_handle]
_library.portproton_gamepad_get_active_instance_id.restype = ctypes.c_uint32
_library.portproton_gamepad_get_name.argtypes = [_handle]
_library.portproton_gamepad_get_name.restype = ctypes.c_char_p
_library.portproton_gamepad_get_instance_id.argtypes = [_handle]
@@ -45,7 +47,7 @@ _library.portproton_gamepad_shutdown.restype = None
class GamepadBackendError(RuntimeError):
"""SDL failed while discovering the active gamepad."""
"""SDL failed while discovering connected gamepads."""
@dataclass
@@ -55,6 +57,7 @@ class SDLGamepad:
path: str
instance_id: int
sdl_type: int
active_instance_id: int = 0
def close(self) -> None:
if self.controller:
@@ -67,8 +70,15 @@ class SDLGamepad:
def connected(self) -> bool:
return bool(_library.portproton_gamepad_connected(self.controller))
def update(self) -> None:
_library.portproton_gamepad_update()
def update(self) -> bool:
_library.portproton_gamepad_update(self.controller)
active_instance_id = int(
_library.portproton_gamepad_get_active_instance_id(self.controller)
)
if active_instance_id == self.active_instance_id:
return False
self.active_instance_id = active_instance_id
return True
def get_button(self, button: int) -> int:
return int(_library.portproton_gamepad_get_button(self.controller, button))
+24
View File
@@ -91,6 +91,30 @@ def test_native_gamepad_result_preserves_python_interface(monkeypatch: MonkeyPat
gamepad.close()
def test_native_gamepad_update_passes_collection_handle(monkeypatch: MonkeyPatch) -> None:
updated_handles: list[int] = []
gamepad = SDLGamepad(42, "Gamepads", "sdl3-gamepad:7", 7, 1)
monkeypatch.setattr(native_gamepad._library, "portproton_gamepad_close", lambda _handle: None)
monkeypatch.setattr(
native_gamepad._library,
"portproton_gamepad_update",
updated_handles.append,
)
monkeypatch.setattr(
native_gamepad._library,
"portproton_gamepad_get_active_instance_id",
lambda _handle: 9,
)
changed = gamepad.update()
assert updated_handles == [42]
assert changed is True
assert gamepad.active_instance_id == 9
assert gamepad.update() is False
gamepad.close()
def test_native_gamepad_reports_sdl_discovery_error(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setattr(native_gamepad._library, "portproton_gamepad_find", lambda: None)
monkeypatch.setattr(