Centralize SDL input subsystem ownership for hotplug recovery
Some checks are pending
Build / setup (push) Waiting to run
Build / build-appimage (push) Blocked by required conditions
Build / build-steamlink (push) Blocked by required conditions
Build / build-windows-macos (push) Blocked by required conditions

This commit is contained in:
Joey Yakimowich-Payne 2026-02-12 07:57:41 -07:00
commit d174341b6d
7 changed files with 315 additions and 117 deletions

View file

@ -179,6 +179,7 @@ SOURCES += \
settings/streamingpreferences.cpp \
streaming/input/abstouch.cpp \
streaming/input/gamepad.cpp \
streaming/input/sdlinputsubsystems.cpp \
streaming/input/input.cpp \
streaming/input/keyboard.cpp \
streaming/input/mouse.cpp \
@ -219,6 +220,7 @@ HEADERS += \
cli/quitstream.h \
cli/startstream.h \
settings/streamingpreferences.h \
streaming/input/sdlinputsubsystems.h \
streaming/input/input.h \
streaming/session.h \
streaming/audio/renderers/renderer.h \

View file

@ -4,10 +4,27 @@
#include <QGuiApplication>
#include <QWindow>
#include "settings/mappingmanager.h"
#include "streaming/input/sdlinputsubsystems.h"
#define AXIS_NAVIGATION_REPEAT_DELAY 150
namespace {
SdlInputSubsystems::LeaseOptions guiNavSubsystemLeaseOptions()
{
SdlInputSubsystems::LeaseOptions options = {};
options.joystick = true;
options.gameController = true;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
options.haptic = false;
#endif
options.applyMappings = true;
options.flushControllerDeviceEvents = true;
return options;
}
}
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation(StreamingPreferences* prefs)
: m_Prefs(prefs),
m_Enabled(false),
@ -31,32 +48,13 @@ void SdlGamepadKeyNavigation::enable()
return;
}
// We have to initialize and uninitialize this in enable()/disable()
// because we need to get out of the way of the Session class. If it
// doesn't get to reinitialize the GC subsystem, it won't get initial
// arrival events. Additionally, there's a race condition between
// our QML objects being destroyed and SDL being deinitialized that
// this solves too.
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
if (!SdlInputSubsystems::acquire("GuiGamepadNavigation", guiNavSubsystemLeaseOptions())) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
"Failed to acquire SDL input subsystems for GUI gamepad navigation");
return;
}
MappingManager mappingManager;
mappingManager.applyMappings();
// Drop all pending gamepad add events. SDL will generate these for us
// on first init of the GC subsystem. We can't depend on them due to
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
// will attach ourselves.
//
// NB: We use SDL_JoystickUpdate() instead of SDL_PumpEvents() because
// the latter can do a bit more work that we want (like handling video
// events that we intentionally do not want to process yet).
SDL_JoystickUpdate();
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
// Open all currently attached game controllers
int numJoysticks = SDL_NumJoysticks();
@ -90,7 +88,7 @@ void SdlGamepadKeyNavigation::disable()
m_Gamepads.removeAt(0);
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SdlInputSubsystems::release("GuiGamepadNavigation", guiNavSubsystemLeaseOptions());
}
void SdlGamepadKeyNavigation::notifyWindowFocus(bool hasFocus)

View file

@ -2,7 +2,7 @@
#include <Limelight.h>
#include "SDL_compat.h"
#include "settings/mappingmanager.h"
#include "streaming/input/sdlinputsubsystems.h"
#include <QtMath>
@ -26,6 +26,8 @@ static bool isKeyboardMouseInputAllowed()
#define HOTPLUG_REENUMERATION_INTERVAL_MS 2000
#define HOTPLUG_REENUMERATION_MAX_INTERVAL_MS 8000
#define HOTPLUG_REENUMERATION_MIN_ZERO_POLLS 4
#define HOTPLUG_REENUMERATION_MIN_LAST_SEEN_MS 500
// Determines how fast the mouse will move each interval
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
@ -105,10 +107,13 @@ void SdlInputHandler::pollForMissingGamepads()
static uint32_t s_LastForcedReenumerationTick = 0;
static uint32_t s_ReenumerationIntervalMs = HOTPLUG_REENUMERATION_INTERVAL_MS;
static uint32_t s_ReenumerationAttempts = 0;
static uint32_t s_ConsecutiveZeroPolls = 0;
SDL_JoystickUpdate();
SDL_GameControllerUpdate();
uint32_t now = SDL_GetTicks();
auto recoverUntrackedGamepads = [this](int joystickCount) {
for (int deviceIndex = 0; deviceIndex < joystickCount; deviceIndex++) {
if (!SDL_IsGameController(deviceIndex)) {
@ -142,9 +147,14 @@ void SdlInputHandler::pollForMissingGamepads()
}
if (joystickCount > 0) {
m_LastNonZeroJoystickTick = now;
s_ConsecutiveZeroPolls = 0;
s_ReenumerationAttempts = 0;
s_ReenumerationIntervalMs = HOTPLUG_REENUMERATION_INTERVAL_MS;
}
else {
s_ConsecutiveZeroPolls++;
}
for (int i = 0; i < MAX_GAMEPADS; i++) {
GamepadState* state = &m_GamepadState[i];
@ -177,40 +187,23 @@ void SdlInputHandler::pollForMissingGamepads()
recoverUntrackedGamepads(joystickCount);
if (joystickCount == 0) {
uint32_t now = SDL_GetTicks();
bool shouldAttemptReenumeration =
joystickCount == 0 &&
m_LastNonZeroJoystickTick != 0 &&
s_ConsecutiveZeroPolls >= HOTPLUG_REENUMERATION_MIN_ZERO_POLLS &&
SDL_TICKS_PASSED(now, m_LastNonZeroJoystickTick + HOTPLUG_REENUMERATION_MIN_LAST_SEEN_MS);
if (shouldAttemptReenumeration) {
if (SDL_TICKS_PASSED(now, s_LastForcedReenumerationTick + s_ReenumerationIntervalMs)) {
s_LastForcedReenumerationTick = now;
s_ReenumerationAttempts++;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"No joysticks visible; forcing SDL joystick/gamecontroller re-enumeration (attempt %u, interval %u ms)",
"No joysticks visible after reconciliation; forcing last-resort SDL re-enumeration (attempt %u, interval %u ms)",
s_ReenumerationAttempts,
s_ReenumerationIntervalMs);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed during re-enumeration: %s",
SDL_GetError());
return;
}
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed during re-enumeration: %s",
SDL_GetError());
return;
}
MappingManager mappingManager;
mappingManager.applyMappings();
SDL_JoystickUpdate();
SDL_GameControllerUpdate();
if (SdlInputSubsystems::reenumerateGamepadSubsystems("StreamInputHotplug")) {
joystickCount = SDL_NumJoysticks();
if (joystickCount != m_LastJoystickCount) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@ -223,6 +216,8 @@ void SdlInputHandler::pollForMissingGamepads()
recoverUntrackedGamepads(joystickCount);
if (joystickCount > 0) {
m_LastNonZeroJoystickTick = now;
s_ConsecutiveZeroPolls = 0;
s_ReenumerationAttempts = 0;
s_ReenumerationIntervalMs = HOTPLUG_REENUMERATION_INTERVAL_MS;
}
@ -231,6 +226,11 @@ void SdlInputHandler::pollForMissingGamepads()
static_cast<uint32_t>(HOTPLUG_REENUMERATION_MAX_INTERVAL_MS));
}
}
else {
s_ReenumerationIntervalMs = qMin(s_ReenumerationIntervalMs * 2,
static_cast<uint32_t>(HOTPLUG_REENUMERATION_MAX_INTERVAL_MS));
}
}
}
}
@ -1236,14 +1236,20 @@ QString SdlInputHandler::getUnmappedGamepads()
{
QString ret;
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
SdlInputSubsystems::LeaseOptions options = {};
options.joystick = true;
options.gameController = true;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
options.haptic = false;
#endif
options.applyMappings = true;
options.flushControllerDeviceEvents = false;
MappingManager mappingManager;
mappingManager.applyMappings();
if (!SdlInputSubsystems::acquire("UnmappedGamepadProbe", options)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to acquire SDL input subsystems for unmapped gamepad probe");
return ret;
}
int numJoysticks = SDL_NumJoysticks();
for (int i = 0; i < numJoysticks; i++) {
@ -1286,7 +1292,7 @@ QString SdlInputHandler::getUnmappedGamepads()
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SdlInputSubsystems::release("UnmappedGamepadProbe", options);
// Flush stale events so they aren't processed by the main session event loop
SDL_FlushEvents(SDL_JOYDEVICEADDED, SDL_JOYDEVICEREMOVED);

View file

@ -1,7 +1,7 @@
#include <Limelight.h>
#include "SDL_compat.h"
#include "streaming/session.h"
#include "settings/mappingmanager.h"
#include "streaming/input/sdlinputsubsystems.h"
#include "path.h"
#include "utils.h"
@ -9,6 +9,23 @@
#include <QDir>
#include <QGuiApplication>
namespace {
SdlInputSubsystems::LeaseOptions streamInputSubsystemLeaseOptions()
{
SdlInputSubsystems::LeaseOptions options = {};
options.joystick = true;
options.gameController = true;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
options.haptic = true;
#endif
options.applyMappings = true;
options.flushControllerDeviceEvents = true;
return options;
}
}
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController),
m_GamepadMouse(prefs.gamepadMouse),
@ -20,6 +37,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i
m_PointerRegionLockActive(false),
m_PointerRegionLockToggledByUser(false),
m_LastJoystickCount(-1),
m_LastNonZeroJoystickTick(0),
m_FakeCaptureActive(false),
m_CaptureSystemKeysMode(prefs.captureSysKeysMode),
m_MouseCursorCapturedVisibilityState(SDL_DISABLE),
@ -178,45 +196,11 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, streamIgnoreDevices.toUtf8());
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, streamIgnoreDevicesExcept.toUtf8());
// We must initialize joystick explicitly before gamecontroller in order
// to ensure we receive gamecontroller attach events for gamepads where
// SDL doesn't have a built-in mapping. By starting joystick first, we
// can allow mapping manager to update the mappings before GC attach
// events are generated.
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
if (!SdlInputSubsystems::acquire("StreamInput", streamInputSubsystemLeaseOptions())) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
SDL_GetError());
"Failed to acquire SDL input subsystems for streaming input");
}
MappingManager mappingManager;
mappingManager.applyMappings();
// Flush gamepad arrival and departure events which may be queued before
// starting the gamecontroller subsystem again. This prevents us from
// receiving duplicate arrival and departure events for the same gamepad.
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
SDL_GetError());
}
#endif
// Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host
// during stream startup as we detect currently attached gamepads one at a time.
@ -250,16 +234,7 @@ SdlInputHandler::~SdlInputHandler()
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
#endif
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
SdlInputSubsystems::release("StreamInput", streamInputSubsystemLeaseOptions());
// Return background event handling to off
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");

View file

@ -235,6 +235,7 @@ private:
int m_GamepadMask;
int m_LastJoystickCount;
uint32_t m_LastNonZeroJoystickTick;
GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown;
bool m_FakeCaptureActive;

View file

@ -0,0 +1,191 @@
#include "streaming/input/sdlinputsubsystems.h"
#include "settings/mappingmanager.h"
namespace {
int s_JoystickRefs = 0;
int s_GameControllerRefs = 0;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
int s_HapticRefs = 0;
#endif
bool retainSubsystem(Uint32 subsystem, int& refs, const char* subsystemName, const char* ownerTag)
{
if (refs == 0 && SDL_InitSubSystem(subsystem) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize %s subsystem for %s: %s",
subsystemName,
ownerTag,
SDL_GetError());
return false;
}
refs++;
return true;
}
void releaseSubsystem(Uint32 subsystem, int& refs, const char* subsystemName, const char* ownerTag)
{
if (refs <= 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Ignoring unbalanced release of %s subsystem for %s",
subsystemName,
ownerTag);
refs = 0;
return;
}
refs--;
if (refs == 0) {
SDL_QuitSubSystem(subsystem);
}
}
}
namespace SdlInputSubsystems {
bool acquire(const char* ownerTag, const LeaseOptions& options)
{
bool acquiredJoystick = false;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
bool acquiredGameController = false;
bool acquiredHaptic = false;
#endif
if (options.joystick) {
if (!retainSubsystem(SDL_INIT_JOYSTICK, s_JoystickRefs, "joystick", ownerTag)) {
return false;
}
acquiredJoystick = true;
}
if (options.gameController) {
if (!retainSubsystem(SDL_INIT_GAMECONTROLLER, s_GameControllerRefs, "gamecontroller", ownerTag)) {
if (acquiredJoystick) {
releaseSubsystem(SDL_INIT_JOYSTICK, s_JoystickRefs, "joystick", ownerTag);
}
return false;
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
acquiredGameController = true;
#endif
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (options.haptic) {
if (!retainSubsystem(SDL_INIT_HAPTIC, s_HapticRefs, "haptic", ownerTag)) {
if (acquiredGameController) {
releaseSubsystem(SDL_INIT_GAMECONTROLLER, s_GameControllerRefs, "gamecontroller", ownerTag);
}
if (acquiredJoystick) {
releaseSubsystem(SDL_INIT_JOYSTICK, s_JoystickRefs, "joystick", ownerTag);
}
return false;
}
acquiredHaptic = true;
}
#endif
if (options.applyMappings) {
MappingManager mappingManager;
mappingManager.applyMappings();
}
if (options.flushControllerDeviceEvents) {
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
}
return true;
}
void release(const char* ownerTag, const LeaseOptions& options)
{
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (options.haptic) {
releaseSubsystem(SDL_INIT_HAPTIC, s_HapticRefs, "haptic", ownerTag);
}
#endif
if (options.gameController) {
releaseSubsystem(SDL_INIT_GAMECONTROLLER, s_GameControllerRefs, "gamecontroller", ownerTag);
}
if (options.joystick) {
releaseSubsystem(SDL_INIT_JOYSTICK, s_JoystickRefs, "joystick", ownerTag);
}
}
bool hasExclusiveGamepadOwnership()
{
if (s_JoystickRefs != 1 || s_GameControllerRefs != 1) {
return false;
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (s_HapticRefs != 1) {
return false;
}
#endif
return true;
}
bool reenumerateGamepadSubsystems(const char* ownerTag)
{
if (!hasExclusiveGamepadOwnership()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Skipping subsystem re-enumeration for %s due to shared ownership (joystick=%d gamecontroller=%d)",
ownerTag,
s_JoystickRefs,
s_GameControllerRefs);
return false;
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
#endif
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to reinitialize joystick subsystem for %s: %s",
ownerTag,
SDL_GetError());
return false;
}
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to reinitialize gamecontroller subsystem for %s: %s",
ownerTag,
SDL_GetError());
return false;
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to reinitialize haptic subsystem for %s: %s",
ownerTag,
SDL_GetError());
return false;
}
#endif
MappingManager mappingManager;
mappingManager.applyMappings();
SDL_JoystickUpdate();
SDL_GameControllerUpdate();
SDL_FlushEvents(SDL_JOYDEVICEADDED, SDL_JOYDEVICEREMOVED);
SDL_FlushEvents(SDL_CONTROLLERDEVICEADDED, SDL_CONTROLLERDEVICEREMAPPED);
return true;
}
}

View file

@ -0,0 +1,25 @@
#pragma once
#include "SDL_compat.h"
namespace SdlInputSubsystems {
struct LeaseOptions {
bool joystick;
bool gameController;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
bool haptic;
#endif
bool applyMappings;
bool flushControllerDeviceEvents;
};
bool acquire(const char* ownerTag, const LeaseOptions& options);
void release(const char* ownerTag, const LeaseOptions& options);
bool hasExclusiveGamepadOwnership();
bool reenumerateGamepadSubsystems(const char* ownerTag);
}