From 27eadf82163759b71cf044f7937af697e04b5504 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 14 Sep 2026 12:53:40 +0500 Subject: [PATCH] fix: support input from multiple gamepads Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 1 + build-aux/lib/libportprotonqt_gamepad.so | Bin 17376 -> 17640 bytes build-aux/src/gamepad_backend.c | 171 ++++++++++++++++++----- portprotonqt/input_manager/runtime.py | 10 +- portprotonqt/native_gamepad.py | 18 ++- tests/test_input_manager.py | 24 ++++ 6 files changed, 183 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d5578b..3703674f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ ### Fixed +- Исправлено управление интерфейсом при одновременном подключении нескольких геймпадов - Исправлена коллизия пользовательских данных автоустановок с одинаковыми именами исходных исполняемых файлов - Исправлен поиск скриптов и локалей через `XDG_DATA_DIRS` в NixOS - Исправлена проверка и установка недостающих i586-зависимостей в ALT Linux перед запуском игр diff --git a/build-aux/lib/libportprotonqt_gamepad.so b/build-aux/lib/libportprotonqt_gamepad.so index 940282bea1eba064fe6caa44daa6dbddc8329732..b0aca6ed886227ca7492eb9003092aff406c4416 100755 GIT binary patch literal 17640 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 mpVYmj?ncA>4zS}3AVS+7SF`jSV)57prQ6 z)(Y6pdSe+lj>3;bNr)6jQR<{wWLaq*m{h_}8ilGPRr9E}krQ19h@mDHYU19WnLC%Q zH&J`h%$fPlH|Lx=J9FDL#e%6=Ut{jf$lTUOBZyM=~s_wF^{-F4~g zu?u(a`Rc-!oE6hp$Wd`%~`Sc3FZ&OUDOMH(?6N-ICx4BodQ27@MeKG z3cN+&=^Qr^QV+kkcG4~=ux*AShkK~T0<*TljGygjYHRN4>JA;-e>i*;j@vdk+uDwX z+xH%7+k4l??!;Eci z|8>VvxNKVuV|Fv-)i}WIxVS+gCjLi+G!gUC+|V%w(@Q{QH#lW;v%VjS$88i-!?x|m zmN-=EV7&JcV-F<~xh(W-?&;`ACusj;f%W8cAYUR033khm&~*4f_R{6?FXfGtX26QH z^E4}7m-k!K>&`ulRVB zNP1Q)F{I&=PZ@m1vJ0;;PWcM85ix!3dZaBXu~wuy*)Y~R6K%HZ&gO4xT68?Z;AnFi z6Ib88xh(%*txEJu1*6)h#LTNo(KSZ2SU#qXApaef5A|^z_07@#v9QeDTz`QDxCgze zE3lggC2&rZGob{F@I7SzU{m2(C~;z}$EQTZKE6ekT8o_?c&joIQkj*B&^0EeSH(it zlwswJ=LDN#v~vobj1jay$}$iQsePeq6s>LwUF)@Bl2j7@6Q?0%n1v7vshEkyeaboy z-&WY7I&W(c?T%eL)t~6$>Uj=ZSze}Xm|^c-gxpD0qP>E(hOv|wQBI77mloC){OL)h~w1uaqFF})b)K`!(@QeTc?VK#M zXwL#aD`EcI>d_fz$}1eiT7S9_{g{O}{GFyPyEv9Ki$~!9n(-`|5mtuK%2jwU*Sm!| z#eB1r^f)kmSMWwmvs24KbZEh-*~!vlOy>DnBtms=E_|Mw2lwY@gFnw|GofAFV!SMG z(iC^DY_<&fS4L>AspdE)Wf`{bZ)n=nC^ye*+8A?)Xy`X88 zD6ga3g>wD3nifWR6y-UTeJDp!UPU>DGVK^yP>7fyZgYLrj_9W4`L(oe-u!hwTFD`RjT~ zKfRxPj96 zeDMuOJdsvPA+f>lV((n`AcH@b!4)Ww5?N3Yc(7<#6eU8CX7=vJN7eK^f{jMuw zZWFK{T)|-BBpxdEXqt9}N#Tg#kHzQx@%G?(DksEu5M960*6Q2@`G%vMTV zWVi_4s3?hya}ycF;4_o#xq^+7|)3@0&aBV)ll=ljTTt%2zB zwvJ~Kb=#c^H#~f|*OiTlV|B#RXCD4xD#&gSXlk{vmsK#?eL|c1segvoH*c}t@(FVM zm=QO<#Zf1(dIG#};r(IWM|uA|>&xH76th_Lk^n0L5%5O>PpyIGp}f*d3Cv@k%liQD z8(6<{AO6_PT53ggO5mvl&$9wgEo}b4ZGOo5>{-D{EoNkJRe4vGn01e-wbypU^<4a% z^NtB#YVCFo@gmHb2mW03qWMVo{=@OVY19msBRa&ZpnO*Ijok#Z8Ha6|t zUf)&`s0iExk?M_*QLR%pISPhq#?$X8oPbvQ+4}h3YR*zt=E8w3579^A>Xx9(xD|#} zz_B%$Y1|z{;$69SYrxenZiOKmAi6au8+T>W0pnY%9DZ@3{6M#Dl#T0kMUHRZ) #include +#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) diff --git a/portprotonqt/input_manager/runtime.py b/portprotonqt/input_manager/runtime.py index b430821a..64168f70 100644 --- a/portprotonqt/input_manager/runtime.py +++ b/portprotonqt/input_manager/runtime.py @@ -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) diff --git a/portprotonqt/native_gamepad.py b/portprotonqt/native_gamepad.py index 3df9bfc9..d9447496 100644 --- a/portprotonqt/native_gamepad.py +++ b/portprotonqt/native_gamepad.py @@ -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)) diff --git a/tests/test_input_manager.py b/tests/test_input_manager.py index b9852431..eb669f3a 100644 --- a/tests/test_input_manager.py +++ b/tests/test_input_manager.py @@ -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( -- 2.54.0