diff --git a/.gitignore b/.gitignore index 3a5180ac..c41d075f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,30 @@ **/.vs/ .vscode/ build/ +.qmake.cache +.qmake.stash +/Makefile +/config.log +/config.tests/ +/app/Makefile +/app/Makefile.Debug +/app/Makefile.Release +/app/release/ +/app/moonlight +/h264bitstream/Makefile +/h264bitstream/Makefile.Debug +/h264bitstream/Makefile.Release +/h264bitstream/release/ +/h264bitstream/libh264bitstream.a +/moonlight-common-c/Makefile +/moonlight-common-c/Makefile.Debug +/moonlight-common-c/Makefile.Release +/moonlight-common-c/release/ +/moonlight-common-c/libmoonlight-common-c.a +/qmdnsengine/Makefile +/qmdnsengine/Makefile.Debug +/qmdnsengine/Makefile.Release +/qmdnsengine/release/ +/qmdnsengine/libqmdnsengine.a config.tests/*/.qmake.stash config.tests/*/Makefile diff --git a/.gitmodules b/.gitmodules index 42c2f1a7..b40a0fa8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "moonlight-common-c/moonlight-common-c"] path = moonlight-common-c/moonlight-common-c - url = https://github.com/moonlight-stream/moonlight-common-c.git + url = git.joeypayne.com:jyapayne/moonlight-common-c [submodule "qmdnsengine/qmdnsengine"] path = qmdnsengine/qmdnsengine url = https://github.com/cgutman/qmdnsengine.git diff --git a/app/app.pro b/app/app.pro index 9fb043dd..717c753e 100644 --- a/app/app.pro +++ b/app/app.pro @@ -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 \ diff --git a/app/gui/sdlgamepadkeynavigation.cpp b/app/gui/sdlgamepadkeynavigation.cpp index d35a70b6..21d44e09 100644 --- a/app/gui/sdlgamepadkeynavigation.cpp +++ b/app/gui/sdlgamepadkeynavigation.cpp @@ -4,10 +4,27 @@ #include #include -#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) diff --git a/app/streaming/audio/audio.cpp b/app/streaming/audio/audio.cpp index 4668eec9..2cbec50b 100644 --- a/app/streaming/audio/audio.cpp +++ b/app/streaming/audio/audio.cpp @@ -8,6 +8,7 @@ #include "renderers/sdl.h" #include +#include #define TRY_INIT_RENDERER(renderer, opusConfig) \ { \ @@ -183,7 +184,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength) s_ActiveSession->m_AudioSampleCount++; // If audio is muted, don't decode or play the audio - if (s_ActiveSession->m_AudioMuted) { + if (s_ActiveSession->m_AudioMuted.load(std::memory_order_relaxed)) { return; } @@ -217,6 +218,27 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength) if (samplesDecoded > 0) { SDL_assert(desiredBufferSize >= frameSize * samplesDecoded); desiredBufferSize = frameSize * samplesDecoded; + + const float volume = s_ActiveSession->getAudioVolumeScalar(); + if (volume <= 0.0f) { + SDL_memset(buffer, 0, desiredBufferSize); + } + else if (volume < 1.0f) { + const int totalSamples = samplesDecoded * s_ActiveSession->m_ActiveAudioConfig.channelCount; + if (s_ActiveSession->m_AudioRenderer->getAudioBufferFormat() == IAudioRenderer::AudioFormat::Float32NE) { + float* output = (float*)buffer; + for (int i = 0; i < totalSamples; i++) { + output[i] = SDL_clamp(output[i] * volume, -1.0f, 1.0f); + } + } + else { + short* output = (short*)buffer; + for (int i = 0; i < totalSamples; i++) { + const int scaled = (int) std::lround((float)output[i] * volume); + output[i] = (short)SDL_clamp(scaled, -32768, 32767); + } + } + } } else { desiredBufferSize = 0; diff --git a/app/streaming/input/abstouch.cpp b/app/streaming/input/abstouch.cpp index 0fe6dac6..6c8529a2 100644 --- a/app/streaming/input/abstouch.cpp +++ b/app/streaming/input/abstouch.cpp @@ -1,4 +1,5 @@ #include "input.h" +#include "streaming/session.h" #include #include "SDL_compat.h" @@ -7,6 +8,12 @@ #include +static bool isKeyboardMouseInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isMouseInputAllowed(); +} + // How long the fingers must be stationary to start a right click #define LONG_PRESS_ACTIVATION_DELAY 650 @@ -22,8 +29,10 @@ Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*) { // Raise the left click and start a right click - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + } return 0; } @@ -110,7 +119,7 @@ void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event) } // Try to send it as a native pen/touch event, otherwise fall back to our touch emulation - if (LiGetHostFeatureFlags() & LI_FF_PEN_TOUCH_EVENTS) { + if (isKeyboardMouseInputAllowed() && (LiGetHostFeatureFlags() & LI_FF_PEN_TOUCH_EVENTS)) { #if SDL_VERSION_ATLEAST(2, 0, 22) bool isPen = false; @@ -197,7 +206,9 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h); // Update the cursor position relative to the video region - LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h); + if (isKeyboardMouseInputAllowed()) { + LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h); + } } if (event->type == SDL_FINGERDOWN) { @@ -210,7 +221,9 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) nullptr); // Left button down on finger down - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } } else if (event->type == SDL_FINGERUP) { m_LastTouchUpEvent = *event; @@ -220,9 +233,13 @@ void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event) m_LongPressTimer = 0; // Left button up on finger up - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } // Raise right button too in case we triggered a long press gesture - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + } } } diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index e0d7f004..6d5601a4 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -2,16 +2,33 @@ #include #include "SDL_compat.h" -#include "settings/mappingmanager.h" +#include "streaming/input/sdlinputsubsystems.h" #include +static bool isGamepadInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isGamepadInputAllowed(); +} + +static bool isMouseInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isMouseInputAllowed(); +} + // How long the Start button must be pressed to toggle mouse emulation #define MOUSE_EMULATION_LONG_PRESS_TIME 750 // How long between polling the gamepad to send virtual mouse input #define MOUSE_EMULATION_POLLING_INTERVAL 50 +#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 @@ -40,7 +57,7 @@ SdlInputHandler::findStateForGamepad(SDL_JoystickID id) int i; for (i = 0; i < MAX_GAMEPADS; i++) { - if (m_GamepadState[i].jsId == id) { + if (m_GamepadState[i].controller != nullptr && m_GamepadState[i].jsId == id) { SDL_assert(!m_MultiController || m_GamepadState[i].index == i); return &m_GamepadState[i]; } @@ -51,6 +68,191 @@ SdlInputHandler::findStateForGamepad(SDL_JoystickID id) return nullptr; } +GamepadState* +SdlInputHandler::ensureStateForGamepad(SDL_JoystickID id) +{ + GamepadState* state = findStateForGamepad(id); + if (state != nullptr) { + return state; + } + + cleanupDetachedGamepads(); + + const int joystickCount = SDL_NumJoysticks(); + for (int deviceIndex = 0; deviceIndex < joystickCount; deviceIndex++) { + if (!SDL_IsGameController(deviceIndex)) { + continue; + } + + if (SDL_JoystickGetDeviceInstanceID(deviceIndex) != id) { + continue; + } + + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Recovering gamepad state for missing add event (instance ID: %d)", + id); + + SDL_ControllerDeviceEvent controllerEvent = {}; + controllerEvent.type = SDL_CONTROLLERDEVICEADDED; + controllerEvent.which = deviceIndex; + handleControllerDeviceEvent(&controllerEvent); + break; + } + + return findStateForGamepad(id); +} + +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)) { + continue; + } + + SDL_JoystickID jsId = SDL_JoystickGetDeviceInstanceID(deviceIndex); + if (jsId < 0 || findStateForGamepad(jsId) != nullptr) { + continue; + } + + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Polling recovered missing gamepad add event (device index: %d, instance ID: %d)", + deviceIndex, + jsId); + + SDL_ControllerDeviceEvent controllerEvent = {}; + controllerEvent.type = SDL_CONTROLLERDEVICEADDED; + controllerEvent.which = deviceIndex; + handleControllerDeviceEvent(&controllerEvent); + } + }; + + int joystickCount = SDL_NumJoysticks(); + if (joystickCount != m_LastJoystickCount) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected joystick count change: %d -> %d", + m_LastJoystickCount, + joystickCount); + m_LastJoystickCount = joystickCount; + } + + 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]; + if (state->controller == nullptr) { + continue; + } + + bool presentInDeviceList = false; + for (int deviceIndex = 0; deviceIndex < joystickCount; deviceIndex++) { + if (SDL_JoystickGetDeviceInstanceID(deviceIndex) == state->jsId) { + presentInDeviceList = true; + break; + } + } + + if (presentInDeviceList) { + continue; + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Polling detected disconnected gamepad instance %d in slot %d; cleaning up", + state->jsId, + i); + + SDL_ControllerDeviceEvent controllerEvent = {}; + controllerEvent.type = SDL_CONTROLLERDEVICEREMOVED; + controllerEvent.which = state->jsId; + handleControllerDeviceEvent(&controllerEvent); + } + + recoverUntrackedGamepads(joystickCount); + + 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 after reconciliation; forcing last-resort SDL re-enumeration (attempt %u, interval %u ms)", + s_ReenumerationAttempts, + s_ReenumerationIntervalMs); + + if (SdlInputSubsystems::reenumerateGamepadSubsystems("StreamInputHotplug")) { + joystickCount = SDL_NumJoysticks(); + if (joystickCount != m_LastJoystickCount) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected joystick count change after re-enumeration: %d -> %d", + m_LastJoystickCount, + joystickCount); + m_LastJoystickCount = joystickCount; + } + + recoverUntrackedGamepads(joystickCount); + + if (joystickCount > 0) { + m_LastNonZeroJoystickTick = now; + s_ConsecutiveZeroPolls = 0; + s_ReenumerationAttempts = 0; + s_ReenumerationIntervalMs = HOTPLUG_REENUMERATION_INTERVAL_MS; + } + else { + s_ReenumerationIntervalMs = qMin(s_ReenumerationIntervalMs * 2, + static_cast(HOTPLUG_REENUMERATION_MAX_INTERVAL_MS)); + } + } + else { + s_ReenumerationIntervalMs = qMin(s_ReenumerationIntervalMs * 2, + static_cast(HOTPLUG_REENUMERATION_MAX_INTERVAL_MS)); + } + } + } +} + +void SdlInputHandler::cleanupDetachedGamepads() +{ + for (int i = 0; i < MAX_GAMEPADS; i++) { + if (m_GamepadState[i].controller == nullptr || + SDL_GameControllerGetAttached(m_GamepadState[i].controller)) { + continue; + } + + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Found detached gamepad in slot %d without removal event; cleaning up", + i); + + SDL_ControllerDeviceEvent controllerEvent = {}; + controllerEvent.type = SDL_CONTROLLERDEVICEREMOVED; + controllerEvent.which = m_GamepadState[i].jsId; + handleControllerDeviceEvent(&controllerEvent); + } +} + void SdlInputHandler::sendGamepadState(GamepadState* state) { SDL_assert(m_GamepadMask == 0x1 || m_MultiController); @@ -101,15 +303,17 @@ void SdlInputHandler::sendGamepadState(GamepadState* state) } } - LiSendMultiControllerEvent(state->index, - m_GamepadMask, - buttons, - lt, - rt, - lsX, - lsY, - rsX, - rsY); + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, + m_GamepadMask, + buttons, + lt, + rt, + lsX, + lsY, + rsX, + rsY); + } } void SdlInputHandler::sendGamepadBatteryState(GamepadState* state, SDL_JoystickPowerLevel level) @@ -150,7 +354,9 @@ void SdlInputHandler::sendGamepadBatteryState(GamepadState* state, SDL_JoystickP return; } - LiSendControllerBatteryEvent(state->index, batteryState, batteryPercentage); + if (isGamepadInputAllowed()) { + LiSendControllerBatteryEvent(state->index, batteryState, batteryPercentage); + } } Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param) @@ -181,7 +387,7 @@ Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0; deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0; - if (deltaX != 0 || deltaY != 0) { + if ((deltaX != 0 || deltaY != 0) && isMouseInputAllowed()) { LiSendMouseMoveEvent((short)deltaX, (short)deltaY); } @@ -191,7 +397,7 @@ Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) { SDL_JoystickID gameControllerId = event->which; - GamepadState* state = findStateForGamepad(gameControllerId); + GamepadState* state = ensureStateForGamepad(gameControllerId); if (state == NULL) { return; } @@ -261,7 +467,7 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve return; } - GamepadState* state = findStateForGamepad(event->which); + GamepadState* state = ensureStateForGamepad(event->which); if (state == NULL) { return; } @@ -291,31 +497,49 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve } else if (state->mouseEmulationTimer != 0) { if (event->button == SDL_CONTROLLER_BUTTON_A) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_B) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_X) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); + } } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) { - LiSendScrollEvent(1); + if (isMouseInputAllowed()) { + LiSendScrollEvent(1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { - LiSendScrollEvent(-1); + if (isMouseInputAllowed()) { + LiSendScrollEvent(-1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) { - LiSendHScrollEvent(1); + if (isMouseInputAllowed()) { + LiSendHScrollEvent(1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) { - LiSendHScrollEvent(-1); + if (isMouseInputAllowed()) { + LiSendHScrollEvent(-1); + } } } } @@ -346,19 +570,29 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve } else if (state->mouseEmulationTimer != 0) { if (event->button == SDL_CONTROLLER_BUTTON_A) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_B) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_X) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); + } } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); + if (isMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); + } } } } @@ -375,8 +609,10 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve SDL_PushEvent(&event); // Clear buttons down on this gamepad - LiSendMultiControllerEvent(state->index, m_GamepadMask, - 0, 0, 0, 0, 0, 0, 0); + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } return; } @@ -390,8 +626,67 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); // Clear buttons down on this gamepad - LiSendMultiControllerEvent(state->index, m_GamepadMask, - 0, 0, 0, 0, 0, 0, 0); + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } + return; + } + + if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | B_FLAG)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected control panel toggle gamepad combo"); + + Session::get()->toggleControlPanelVisibility(); + + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } + return; + } + + if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | Y_FLAG)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected keyboard input toggle gamepad combo"); + + Session::get()->toggleKeyboardInputAllowed(); + + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } + return; + } + + if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | UP_FLAG)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected mouse input toggle gamepad combo"); + + Session::get()->toggleMouseInputAllowed(); + + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } + return; + } + + if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | A_FLAG)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected gamepad input toggle gamepad combo"); + + bool gamepadWasEnabled = isGamepadInputAllowed(); + if (gamepadWasEnabled) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } + Session::get()->toggleGamepadInputAllowed(); + + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } return; } @@ -405,7 +700,7 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve void SdlInputHandler::handleControllerSensorEvent(SDL_ControllerSensorEvent* event) { - GamepadState* state = findStateForGamepad(event->which); + GamepadState* state = ensureStateForGamepad(event->which); if (state == NULL) { return; } @@ -418,7 +713,9 @@ void SdlInputHandler::handleControllerSensorEvent(SDL_ControllerSensorEvent* eve memcpy(state->lastAccelEventData, event->data, sizeof(event->data)); state->lastAccelEventTime = event->timestamp; - LiSendControllerMotionEvent((uint8_t)state->index, LI_MOTION_TYPE_ACCEL, event->data[0], event->data[1], event->data[2]); + if (isGamepadInputAllowed()) { + LiSendControllerMotionEvent((uint8_t)state->index, LI_MOTION_TYPE_ACCEL, event->data[0], event->data[1], event->data[2]); + } } break; case SDL_SENSOR_GYRO: @@ -429,10 +726,12 @@ void SdlInputHandler::handleControllerSensorEvent(SDL_ControllerSensorEvent* eve state->lastGyroEventTime = event->timestamp; // Convert rad/s to deg/s - LiSendControllerMotionEvent((uint8_t)state->index, LI_MOTION_TYPE_GYRO, - event->data[0] * 57.2957795f, - event->data[1] * 57.2957795f, - event->data[2] * 57.2957795f); + if (isGamepadInputAllowed()) { + LiSendControllerMotionEvent((uint8_t)state->index, LI_MOTION_TYPE_GYRO, + event->data[0] * 57.2957795f, + event->data[1] * 57.2957795f, + event->data[2] * 57.2957795f); + } } break; } @@ -440,7 +739,7 @@ void SdlInputHandler::handleControllerSensorEvent(SDL_ControllerSensorEvent* eve void SdlInputHandler::handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* event) { - GamepadState* state = findStateForGamepad(event->which); + GamepadState* state = ensureStateForGamepad(event->which); if (state == NULL) { return; } @@ -460,7 +759,9 @@ void SdlInputHandler::handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* return; } - LiSendControllerTouchEvent((uint8_t)state->index, eventType, event->finger, event->x, event->y, event->pressure); + if (isGamepadInputAllowed()) { + LiSendControllerTouchEvent((uint8_t)state->index, eventType, event->finger, event->x, event->y, event->pressure); + } } #endif @@ -469,7 +770,7 @@ void SdlInputHandler::handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* void SdlInputHandler::handleJoystickBatteryEvent(SDL_JoyBatteryEvent* event) { - GamepadState* state = findStateForGamepad(event->which); + GamepadState* state = ensureStateForGamepad(event->which); if (state == NULL) { return; } @@ -484,6 +785,8 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve GamepadState* state; if (event->type == SDL_CONTROLLERDEVICEADDED) { + cleanupDetachedGamepads(); + int i; const char* name; SDL_GameController* controller; @@ -499,15 +802,17 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve return; } + SDL_JoystickID jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)); + // SDL_CONTROLLERDEVICEADDED can be reported multiple times for the same // gamepad in rare cases, because SDL doesn't fixup the device index in // the SDL_CONTROLLERDEVICEADDED event if an unopened gamepad disappears // before we've processed the add event. for (int i = 0; i < MAX_GAMEPADS; i++) { - if (m_GamepadState[i].controller == controller) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Received duplicate add event for controller index: %d", - event->which); + if (m_GamepadState[i].controller != nullptr && m_GamepadState[i].jsId == jsId) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Received duplicate add event for joystick instance ID: %d", + jsId); SDL_GameControllerClose(controller); return; } @@ -563,7 +868,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve } state->controller = controller; - state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller)); + state->jsId = jsId; hapticCaps = 0; #if SDL_VERSION_ATLEAST(2, 0, 18) @@ -708,7 +1013,9 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve #endif type == LI_CTYPE_PS; - LiSendControllerArrivalEvent(state->index, m_GamepadMask, type, supportedButtonFlags, capabilities); + if (isGamepadInputAllowed()) { + LiSendControllerArrivalEvent(state->index, m_GamepadMask, type, supportedButtonFlags, capabilities); + } #else // Send an empty event to tell the PC we've arrived @@ -750,8 +1057,10 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve state->index); // Send a final event to let the PC know this gamepad is gone - LiSendMultiControllerEvent(state->index, m_GamepadMask, - 0, 0, 0, 0, 0, 0, 0); + if (isGamepadInputAllowed()) { + LiSendMultiControllerEvent(state->index, m_GamepadMask, + 0, 0, 0, 0, 0, 0, 0); + } // Clear all remaining state from this slot SDL_memset(state, 0, sizeof(*state)); @@ -763,29 +1072,49 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) { SDL_assert(event->type == SDL_JOYDEVICEADDED); - if (!SDL_IsGameController(event->which)) { - char guidStr[33]; - SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which), - guidStr, sizeof(guidStr)); - const char* name = SDL_JoystickNameForIndex(event->which); - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Joystick discovered with no mapping: %s %s", - name ? name : "", - guidStr); - SDL_Joystick* joy = SDL_JoystickOpen(event->which); - if (joy != nullptr) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Number of axes: %d | Number of buttons: %d | Number of hats: %d", - SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy), - SDL_JoystickNumHats(joy)); - SDL_JoystickClose(joy); - } - else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Unable to open joystick for query: %s", - SDL_GetError()); - } + if (SDL_IsGameController(event->which)) { + SDL_ControllerDeviceEvent controllerEvent = {}; + controllerEvent.type = SDL_CONTROLLERDEVICEADDED; + controllerEvent.which = event->which; + handleControllerDeviceEvent(&controllerEvent); + return; } + + char guidStr[33]; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which), + guidStr, sizeof(guidStr)); + const char* name = SDL_JoystickNameForIndex(event->which); + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Joystick discovered with no mapping: %s %s", + name ? name : "", + guidStr); + SDL_Joystick* joy = SDL_JoystickOpen(event->which); + if (joy != nullptr) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Number of axes: %d | Number of buttons: %d | Number of hats: %d", + SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy), + SDL_JoystickNumHats(joy)); + SDL_JoystickClose(joy); + } + else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Unable to open joystick for query: %s", + SDL_GetError()); + } +} + +void SdlInputHandler::handleJoystickRemovalEvent(SDL_JoyDeviceEvent* event) +{ + SDL_assert(event->type == SDL_JOYDEVICEREMOVED); + + if (findStateForGamepad(event->which) == nullptr) { + return; + } + + SDL_ControllerDeviceEvent controllerEvent = {}; + controllerEvent.type = SDL_CONTROLLERDEVICEREMOVED; + controllerEvent.which = event->which; + handleControllerDeviceEvent(&controllerEvent); } void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) @@ -920,14 +1249,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++) { @@ -970,7 +1305,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); diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 982eaaa9..39a230a5 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -1,7 +1,7 @@ #include #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 #include +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), @@ -19,6 +36,8 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i m_PendingMouseButtonsAllUpOnVideoRegionLeave(false), m_PointerRegionLockActive(false), m_PointerRegionLockToggledByUser(false), + m_LastJoystickCount(-1), + m_LastNonZeroJoystickTick(0), m_FakeCaptureActive(false), m_CaptureSystemKeysMode(prefs.captureSysKeysMode), m_MouseCursorCapturedVisibilityState(SDL_DISABLE), @@ -86,6 +105,11 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i m_SpecialKeyCombos[KeyComboToggleStatsOverlay].scanCode = SDL_SCANCODE_S; m_SpecialKeyCombos[KeyComboToggleStatsOverlay].enabled = true; + m_SpecialKeyCombos[KeyComboToggleControlPanel].keyCombo = KeyComboToggleControlPanel; + m_SpecialKeyCombos[KeyComboToggleControlPanel].keyCode = SDLK_p; + m_SpecialKeyCombos[KeyComboToggleControlPanel].scanCode = SDL_SCANCODE_P; + m_SpecialKeyCombos[KeyComboToggleControlPanel].enabled = true; + m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCombo = KeyComboToggleMouseMode; m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCode = SDLK_m; m_SpecialKeyCombos[KeyComboToggleMouseMode].scanCode = SDL_SCANCODE_M; @@ -106,6 +130,36 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V; m_SpecialKeyCombos[KeyComboPasteText].enabled = true; + m_SpecialKeyCombos[KeyComboToggleKeyboardInput].keyCombo = KeyComboToggleKeyboardInput; + m_SpecialKeyCombos[KeyComboToggleKeyboardInput].keyCode = SDLK_k; + m_SpecialKeyCombos[KeyComboToggleKeyboardInput].scanCode = SDL_SCANCODE_K; + m_SpecialKeyCombos[KeyComboToggleKeyboardInput].enabled = true; + + m_SpecialKeyCombos[KeyComboToggleMouseInput].keyCombo = KeyComboToggleMouseInput; + m_SpecialKeyCombos[KeyComboToggleMouseInput].keyCode = SDLK_o; + m_SpecialKeyCombos[KeyComboToggleMouseInput].scanCode = SDL_SCANCODE_O; + m_SpecialKeyCombos[KeyComboToggleMouseInput].enabled = true; + + m_SpecialKeyCombos[KeyComboToggleGamepadInput].keyCombo = KeyComboToggleGamepadInput; + m_SpecialKeyCombos[KeyComboToggleGamepadInput].keyCode = SDLK_g; + m_SpecialKeyCombos[KeyComboToggleGamepadInput].scanCode = SDL_SCANCODE_G; + m_SpecialKeyCombos[KeyComboToggleGamepadInput].enabled = true; + + m_SpecialKeyCombos[KeyComboVolumeUp].keyCombo = KeyComboVolumeUp; + m_SpecialKeyCombos[KeyComboVolumeUp].keyCode = SDLK_u; + m_SpecialKeyCombos[KeyComboVolumeUp].scanCode = SDL_SCANCODE_U; + m_SpecialKeyCombos[KeyComboVolumeUp].enabled = true; + + m_SpecialKeyCombos[KeyComboVolumeDown].keyCombo = KeyComboVolumeDown; + m_SpecialKeyCombos[KeyComboVolumeDown].keyCode = SDLK_j; + m_SpecialKeyCombos[KeyComboVolumeDown].scanCode = SDL_SCANCODE_J; + m_SpecialKeyCombos[KeyComboVolumeDown].enabled = true; + + m_SpecialKeyCombos[KeyComboToggleMute].keyCombo = KeyComboToggleMute; + m_SpecialKeyCombos[KeyComboToggleMute].keyCode = SDLK_n; + m_SpecialKeyCombos[KeyComboToggleMute].scanCode = SDL_SCANCODE_N; + m_SpecialKeyCombos[KeyComboToggleMute].enabled = true; + m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].keyCombo = KeyComboTogglePointerRegionLock; m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].keyCode = SDLK_l; m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].scanCode = SDL_SCANCODE_L; @@ -147,45 +201,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. @@ -219,16 +239,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"); @@ -268,6 +279,50 @@ void SdlInputHandler::raiseAllKeys() m_KeysDown.clear(); } +void SdlInputHandler::raiseAllMouseButtons() +{ + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); +} + +void SdlInputHandler::raiseAllGamepadInputs() +{ + uint16_t raisedIndexMask = 0; + + for (auto &state : m_GamepadState) { + if (state.controller == nullptr) { + continue; + } + + state.buttons = 0; + state.lt = 0; + state.rt = 0; + state.lsX = 0; + state.lsY = 0; + state.rsX = 0; + state.rsY = 0; + state.emulatedClickpadButtonDown = false; + + if ((raisedIndexMask & (1 << state.index)) != 0) { + continue; + } + + LiSendMultiControllerEvent(state.index, + m_GamepadMask, + 0, + 0, + 0, + 0, + 0, + 0, + 0); + raisedIndexMask |= (1 << state.index); + } +} + void SdlInputHandler::notifyMouseLeave() { // SDL on Windows doesn't send the mouse button up until the mouse re-enters the window diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 20bbbc33..46e3ae79 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -116,6 +116,10 @@ public: void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); + void handleJoystickRemovalEvent(SDL_JoyDeviceEvent* event); + + void pollForMissingGamepads(); + void sendText(QString& string); void rumble(uint16_t controllerNumber, uint16_t lowFreqMotor, uint16_t highFreqMotor); @@ -134,6 +138,10 @@ public: void raiseAllKeys(); + void raiseAllMouseButtons(); + + void raiseAllGamepadInputs(); + void notifyMouseLeave(); void notifyFocusLost(); @@ -161,10 +169,17 @@ private: KeyComboUngrabInput, KeyComboToggleFullScreen, KeyComboToggleStatsOverlay, + KeyComboToggleControlPanel, KeyComboToggleMouseMode, KeyComboToggleCursorHide, KeyComboToggleMinimize, KeyComboPasteText, + KeyComboToggleKeyboardInput, + KeyComboToggleMouseInput, + KeyComboToggleGamepadInput, + KeyComboVolumeUp, + KeyComboVolumeDown, + KeyComboToggleMute, KeyComboTogglePointerRegionLock, KeyComboQuitAndExit, KeyComboMax @@ -173,6 +188,11 @@ private: GamepadState* findStateForGamepad(SDL_JoystickID id); + GamepadState* + ensureStateForGamepad(SDL_JoystickID id); + + void cleanupDetachedGamepads(); + void sendGamepadState(GamepadState* state); void sendGamepadBatteryState(GamepadState* state, SDL_JoystickPowerLevel level); @@ -215,6 +235,8 @@ private: bool m_PointerRegionLockToggledByUser; int m_GamepadMask; + int m_LastJoystickCount; + uint32_t m_LastNonZeroJoystickTick; GamepadState m_GamepadState[MAX_GAMEPADS]; QSet m_KeysDown; bool m_FakeCaptureActive; diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index 273816f4..ede74d51 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -13,6 +13,12 @@ #define VK_NUMPAD0 0x60 #endif +static bool isKeyboardMouseInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isKeyboardInputAllowed(); +} + void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) { switch (combo) { @@ -58,6 +64,14 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)); break; + case KeyComboToggleControlPanel: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected control panel toggle combo"); + if (auto session = Session::get(); session != nullptr) { + session->toggleControlPanelVisibility(); + } + break; + case KeyComboToggleMouseMode: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected mouse mode toggle combo"); @@ -114,7 +128,9 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) } // Send this text to the PC - LiSendUtf8TextEvent(text, (unsigned int)strlen(text)); + if (isKeyboardMouseInputAllowed()) { + LiSendUtf8TextEvent(text, (unsigned int)strlen(text)); + } // SDL_GetClipboardText() allocates, so we must free SDL_free((void*)text); @@ -126,6 +142,54 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) break; } + case KeyComboToggleKeyboardInput: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected keyboard input toggle combo"); + if (auto session = Session::get(); session != nullptr) { + session->toggleKeyboardInputAllowed(); + } + break; + + case KeyComboToggleMouseInput: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected mouse input toggle combo"); + if (auto session = Session::get(); session != nullptr) { + session->toggleMouseInputAllowed(); + } + break; + + case KeyComboToggleGamepadInput: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected gamepad input toggle combo"); + if (auto session = Session::get(); session != nullptr) { + session->toggleGamepadInputAllowed(); + } + break; + + case KeyComboVolumeUp: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected volume up combo"); + if (auto session = Session::get(); session != nullptr) { + session->adjustAudioVolume(0.05f); + } + break; + + case KeyComboVolumeDown: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected volume down combo"); + if (auto session = Session::get(); session != nullptr) { + session->adjustAudioVolume(-0.05f); + } + break; + + case KeyComboToggleMute: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected audio mute toggle combo"); + if (auto session = Session::get(); session != nullptr) { + session->toggleAudioMute(); + } + break; + case KeyComboTogglePointerRegionLock: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected pointer region lock toggle combo"); @@ -457,9 +521,11 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) m_KeysDown.remove(keyCode); } - LiSendKeyboardEvent2(0x8000 | keyCode, - event->state == SDL_PRESSED ? - KEY_ACTION_DOWN : KEY_ACTION_UP, - modifiers, - shouldNotConvertToScanCodeOnServer ? SS_KBE_FLAG_NON_NORMALIZED : 0); + if (isKeyboardMouseInputAllowed()) { + LiSendKeyboardEvent2(0x8000 | keyCode, + event->state == SDL_PRESSED ? + KEY_ACTION_DOWN : KEY_ACTION_UP, + modifiers, + shouldNotConvertToScanCodeOnServer ? SS_KBE_FLAG_NON_NORMALIZED : 0); + } } diff --git a/app/streaming/input/mouse.cpp b/app/streaming/input/mouse.cpp index 66bb4413..d452ed07 100644 --- a/app/streaming/input/mouse.cpp +++ b/app/streaming/input/mouse.cpp @@ -1,9 +1,16 @@ #include "input.h" +#include "streaming/session.h" #include #include "SDL_compat.h" #include "streaming/streamutils.h" +static bool isKeyboardMouseInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isMouseInputAllowed(); +} + void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) { int button; @@ -62,10 +69,12 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) button = BUTTON_RIGHT; } - LiSendMouseButtonEvent(event->state == SDL_PRESSED ? - BUTTON_ACTION_PRESS : - BUTTON_ACTION_RELEASE, - button); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(event->state == SDL_PRESSED ? + BUTTON_ACTION_PRESS : + BUTTON_ACTION_RELEASE, + button); + } } void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) @@ -134,7 +143,9 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) } } if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) { - LiSendMousePositionEvent((short)x, (short)y, dst.w, dst.h); + if (isKeyboardMouseInputAllowed()) { + LiSendMousePositionEvent((short)x, (short)y, dst.w, dst.h); + } } // Adjust the cursor visibility if applicable @@ -150,7 +161,9 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) m_MouseWasInVideoRegion = mouseInVideoRegion; } else { - LiSendMouseMoveEvent(xrel, yrel); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseMoveEvent(xrel, yrel); + } } } @@ -187,7 +200,9 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) event->preciseY = SDL_clamp(event->preciseY, -1.0f, 1.0f); #endif - LiSendHighResScrollEvent((short)(event->preciseY * 120)); // WHEEL_DELTA + if (isKeyboardMouseInputAllowed()) { + LiSendHighResScrollEvent((short)(event->preciseY * 120)); // WHEEL_DELTA + } } if (event->preciseX != 0.0f) { @@ -202,7 +217,9 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) event->preciseX = SDL_clamp(event->preciseX, -1.0f, 1.0f); #endif - LiSendHighResHScrollEvent((short)(event->preciseX * 120)); // WHEEL_DELTA + if (isKeyboardMouseInputAllowed()) { + LiSendHighResHScrollEvent((short)(event->preciseX * 120)); // WHEEL_DELTA + } } #else if (event->y != 0) { @@ -216,7 +233,9 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) event->y = SDL_clamp(event->y, -1, 1); #endif - LiSendScrollEvent((signed char)event->y); + if (isKeyboardMouseInputAllowed()) { + LiSendScrollEvent((signed char)event->y); + } } if (event->x != 0) { @@ -230,7 +249,9 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) event->x = SDL_clamp(event->x, -1, 1); #endif - LiSendHScrollEvent((signed char)event->x); + if (isKeyboardMouseInputAllowed()) { + LiSendHScrollEvent((signed char)event->x); + } } #endif } diff --git a/app/streaming/input/reltouch.cpp b/app/streaming/input/reltouch.cpp index 0ddc778a..6269537f 100644 --- a/app/streaming/input/reltouch.cpp +++ b/app/streaming/input/reltouch.cpp @@ -1,10 +1,17 @@ #include "input.h" +#include "streaming/session.h" #include #include "SDL_compat.h" #include +static bool isKeyboardMouseInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isMouseInputAllowed(); +} + // How long the mouse button will be pressed for a tap to click gesture #define TAP_BUTTON_RELEASE_DELAY 100 @@ -16,13 +23,17 @@ Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } return 0; } Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + } return 0; } @@ -39,7 +50,9 @@ Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param) me->m_DragButton = BUTTON_LEFT; } - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton); + } return 0; } @@ -99,7 +112,7 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) // than the client window dimensions. short deltaX = static_cast(event->dx * m_StreamWidth); short deltaY = static_cast(event->dy * m_StreamHeight); - if (deltaX != 0 || deltaY != 0) { + if ((deltaX != 0 || deltaY != 0) && isKeyboardMouseInputAllowed()) { LiSendMouseMoveEvent(deltaX, deltaY); } } @@ -133,7 +146,9 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) // Release any drag if (m_DragButton != 0) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton); + } m_DragButton = 0; } // 2 finger tap @@ -143,7 +158,9 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) m_TouchDownEvent[0].timestamp = 0; // Press down the right mouse button - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + } // Queue a timer to release it in 100 ms SDL_RemoveTimer(m_RightButtonReleaseTimer); @@ -154,7 +171,9 @@ void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event) // 1 finger tap else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) { // Press down the left mouse button - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } // Queue a timer to release it in 100 ms SDL_RemoveTimer(m_LeftButtonReleaseTimer); diff --git a/app/streaming/input/sdlinputsubsystems.cpp b/app/streaming/input/sdlinputsubsystems.cpp new file mode 100644 index 00000000..bb1d3646 --- /dev/null +++ b/app/streaming/input/sdlinputsubsystems.cpp @@ -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; +} + +} diff --git a/app/streaming/input/sdlinputsubsystems.h b/app/streaming/input/sdlinputsubsystems.h new file mode 100644 index 00000000..c5e4e3c5 --- /dev/null +++ b/app/streaming/input/sdlinputsubsystems.h @@ -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); + +} diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index ad3741b0..01aa4800 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -28,6 +28,8 @@ #define SDL_CODE_GAMECONTROLLER_SET_MOTION_EVENT_STATE 103 #define SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED 104 #define SDL_CODE_GAMECONTROLLER_SET_ADAPTIVE_TRIGGERS 105 +#define SDL_CODE_HIDE_STATUS_OVERLAY 106 +#define SDL_CODE_SET_INPUT_POLICY 107 #include @@ -60,7 +62,8 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { Session::clRumbleTriggers, Session::clSetMotionEventState, Session::clSetControllerLED, - Session::clSetAdaptiveTriggers + Session::clSetAdaptiveTriggers, + Session::clSetInputPolicy }; Session* Session::s_ActiveSession; @@ -175,27 +178,8 @@ void Session::clConnectionStatusUpdate(int connectionStatus) "Connection status update: %d", connectionStatus); - if (!s_ActiveSession->m_Preferences->connectionWarnings) { - return; - } - - if (s_ActiveSession->m_MouseEmulationRefCount > 0) { - // Don't display the overlay if mouse emulation is already using it - return; - } - - switch (connectionStatus) - { - case CONN_STATUS_POOR: - s_ActiveSession->m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, - s_ActiveSession->m_StreamConfig.bitrate > 5000 ? - "Slow connection to PC\nReduce your bitrate" : "Poor connection to PC"); - s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); - break; - case CONN_STATUS_OKAY: - s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); - break; - } + s_ActiveSession->m_ConnectionStatus.store(connectionStatus, std::memory_order_relaxed); + s_ActiveSession->refreshControlPanelOverlay(); } void Session::clSetHdrMode(bool enabled) @@ -274,6 +258,20 @@ void Session::clSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlag SDL_PushEvent(&setControllerLEDEvent); } +void Session::clSetInputPolicy(uint8_t allowKeyboard, uint8_t allowMouse, uint8_t allowGamepad, uint8_t reason) +{ + SDL_Event setInputPolicyEvent = {}; + setInputPolicyEvent.type = SDL_USEREVENT; + setInputPolicyEvent.user.code = SDL_CODE_SET_INPUT_POLICY; + setInputPolicyEvent.user.data1 = nullptr; + setInputPolicyEvent.user.data2 = (void*)(uintptr_t)( + ((uintptr_t)allowKeyboard << 24) | + ((uintptr_t)allowMouse << 16) | + ((uintptr_t)allowGamepad << 8) | + (uintptr_t)reason); + SDL_PushEvent(&setInputPolicyEvent); +} + bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, SDL_Window* window, int videoFormat, int width, int height, @@ -556,6 +554,14 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_VideoDecoder(nullptr), m_DecoderLock(SDL_CreateMutex()), m_AudioMuted(false), + m_ManualAudioMuted(false), + m_AudioVolumeScalar(1.0f), + m_AllowGamepadInput(true), + m_AllowKeyboardInput(false), + m_AllowMouseInput(false), + m_ControlPanelVisible(true), + m_ConnectionStatus(CONN_STATUS_OKAY), + m_StatusOverlayGeneration(0), m_QtWindow(nullptr), m_UnexpectedTermination(true), // Failure prior to streaming is unexpected m_InputHandler(nullptr), @@ -569,6 +575,285 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_AudioSampleCount(0), m_DropAudioEndTime(0) { + m_TemporaryStatusOverlayText[0] = '\0'; +} + +void Session::setGamepadInputAllowed(bool allowed) +{ + if (!allowed && m_InputHandler != nullptr) { + m_InputHandler->raiseAllGamepadInputs(); + } + + m_AllowGamepadInput.store(allowed, std::memory_order_relaxed); + sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_USER_TOGGLE); + notifyInputPermissionState(); +} + +void Session::setKeyboardInputAllowed(bool allowed) +{ + const bool previous = m_AllowKeyboardInput.exchange(allowed, std::memory_order_relaxed); + if (previous && !allowed && m_InputHandler != nullptr) { + m_InputHandler->raiseAllKeys(); + } + + sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_USER_TOGGLE); + notifyInputPermissionState(); +} + +void Session::setMouseInputAllowed(bool allowed) +{ + const bool previous = m_AllowMouseInput.exchange(allowed, std::memory_order_relaxed); + if (previous && !allowed && m_InputHandler != nullptr) { + m_InputHandler->raiseAllMouseButtons(); + } + + sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_USER_TOGGLE); + notifyInputPermissionState(); +} + +void Session::setAudioVolumeScalar(float scalar) +{ + scalar = SDL_clamp(scalar, 0.0f, 1.0f); + m_AudioVolumeScalar.store(scalar, std::memory_order_relaxed); + notifyAudioVolumeState(); +} + +void Session::adjustAudioVolume(float delta) +{ + const float current = getAudioVolumeScalar(); + const float next = SDL_clamp(current + delta, 0.0f, 1.0f); + setAudioVolumeScalar(next); +} + +void Session::toggleAudioMute() +{ + const bool muted = !m_ManualAudioMuted.load(std::memory_order_relaxed); + m_ManualAudioMuted.store(muted, std::memory_order_relaxed); + + updateEffectiveAudioMuteState(); + notifyAudioVolumeState(); +} + +void Session::notifyAudioVolumeState() +{ + char buffer[64]; + if (m_ManualAudioMuted.load(std::memory_order_relaxed)) { + SDL_snprintf(buffer, sizeof(buffer), "Volume: MUTED"); + } + else { + const int percent = (int)SDL_roundf(getAudioVolumeScalar() * 100.0f); + SDL_snprintf(buffer, sizeof(buffer), "Volume: %d%%", percent); + } + + showTemporaryStatusOverlay(buffer); +} + +void Session::setKeyboardMouseInputAllowed(bool allowed) +{ + const bool previousKeyboard = m_AllowKeyboardInput.exchange(allowed, std::memory_order_relaxed); + if (previousKeyboard && !allowed && m_InputHandler != nullptr) { + m_InputHandler->raiseAllKeys(); + } + + const bool previousMouse = m_AllowMouseInput.exchange(allowed, std::memory_order_relaxed); + if (previousMouse && !allowed && m_InputHandler != nullptr) { + m_InputHandler->raiseAllMouseButtons(); + } + + sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_USER_TOGGLE); + notifyInputPermissionState(); +} + +void Session::toggleGamepadInputAllowed() +{ + setGamepadInputAllowed(!isGamepadInputAllowed()); +} + +void Session::toggleKeyboardInputAllowed() +{ + setKeyboardInputAllowed(!isKeyboardInputAllowed()); +} + +void Session::toggleMouseInputAllowed() +{ + setMouseInputAllowed(!isMouseInputAllowed()); +} + +void Session::toggleKeyboardMouseInputAllowed() +{ + setKeyboardMouseInputAllowed(!isKeyboardMouseInputAllowed()); +} + +void Session::toggleControlPanelVisibility() +{ + const bool visible = !m_ControlPanelVisible.load(std::memory_order_relaxed); + m_ControlPanelVisible.store(visible, std::memory_order_relaxed); + showTemporaryStatusOverlay(visible ? "Control panel shown" : "Control panel hidden"); +} + +void Session::notifyInputPermissionState() +{ + char buffer[160]; + SDL_snprintf(buffer, + sizeof(buffer), + "Input policy: host controlled\nKeyboard: %s\nMouse: %s\nGamepad: %s", + isKeyboardInputAllowed() ? "ON" : "OFF", + isMouseInputAllowed() ? "ON" : "OFF", + isGamepadInputAllowed() ? "ON" : "OFF"); + + showTemporaryStatusOverlay(buffer); +} + +void Session::sendInputPermissionStateToHost(uint8_t reason) +{ + const bool allowKeyboard = isKeyboardInputAllowed(); + const bool allowMouse = isMouseInputAllowed(); + const bool allowGamepad = isGamepadInputAllowed(); + const int err = LiSendSessionInputPolicy(allowKeyboard, + allowMouse, + allowGamepad, + reason); + + if (err != 0 && err != LI_ERR_UNSUPPORTED) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Failed to send input policy update to host: %d", + err); + } +} + +Uint32 Session::statusOverlayTimeoutCallback(Uint32, void* param) +{ + SDL_Event event = {}; + event.type = SDL_USEREVENT; + event.user.code = SDL_CODE_HIDE_STATUS_OVERLAY; + event.user.data1 = param; + SDL_PushEvent(&event); + + return 0; +} + +void Session::refreshControlPanelOverlay() +{ + if (!m_ControlPanelVisible.load(std::memory_order_relaxed)) { + if (m_TemporaryStatusOverlayText[0] != '\0') { + m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, m_TemporaryStatusOverlayText); + m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); + } + else { + m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); + } + return; + } + + char volumeState[24]; + if (m_ManualAudioMuted.load(std::memory_order_relaxed)) { + SDL_snprintf(volumeState, sizeof(volumeState), "MUTED"); + } + else { + const int percent = (int)SDL_roundf(getAudioVolumeScalar() * 100.0f); + SDL_snprintf(volumeState, sizeof(volumeState), "%d%%", percent); + } + + char panelText[640]; + SDL_snprintf(panelText, + sizeof(panelText), + "Stream Controls\n" + "Keyboard: %s (KB: Ctrl+Alt+Shift+K, Pad: Select+L1+R1+Y)\n" + "Mouse: %s (KB: Ctrl+Alt+Shift+O, Pad: Select+L1+R1+DPad Up)\n" + "Pad: %s (KB: Ctrl+Alt+Shift+G, Pad: Select+L1+R1+A)\n" + "Vol: %s (KB: U/J, mute N; Pad: none)\n" + "UI: ON (KB: Ctrl+Alt+Shift+P, Pad: Select+L1+R1+B)\n" + "Input policy: host controlled", + isKeyboardInputAllowed() ? "ON" : "OFF", + isMouseInputAllowed() ? "ON" : "OFF", + isGamepadInputAllowed() ? "ON" : "OFF", + volumeState); + + size_t used = SDL_strlen(panelText); + if (used < sizeof(panelText) && m_MouseEmulationRefCount > 0) { + SDL_snprintf(panelText + used, + sizeof(panelText) - used, + "\nMouse mode: ACTIVE (hold Start to disable)"); + used = SDL_strlen(panelText); + } + + if (used < sizeof(panelText) && + m_Preferences->connectionWarnings && + m_ConnectionStatus.load(std::memory_order_relaxed) == CONN_STATUS_POOR) { + SDL_snprintf(panelText + used, + sizeof(panelText) - used, + "\nNetwork: %s", + m_StreamConfig.bitrate > 5000 ? "Slow (reduce bitrate)" : "Poor"); + used = SDL_strlen(panelText); + } + + if (used < sizeof(panelText) && m_TemporaryStatusOverlayText[0] != '\0') { + SDL_snprintf(panelText + used, + sizeof(panelText) - used, + "\nStatus: %s", + m_TemporaryStatusOverlayText); + } + + m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, panelText); + m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); +} + +void Session::showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs) +{ + if (text == nullptr || text[0] == '\0') { + return; + } + + SDL_snprintf(m_TemporaryStatusOverlayText, + sizeof(m_TemporaryStatusOverlayText), + "%s", + text); + refreshControlPanelOverlay(); + + const uint32_t generation = m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed) + 1; + SDL_AddTimer(timeoutMs, statusOverlayTimeoutCallback, (void*)(uintptr_t)generation); +} + +void Session::applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool allowGamepad, uint8_t reason) +{ + const bool previousKeyboard = m_AllowKeyboardInput.exchange(allowKeyboard, std::memory_order_relaxed); + if (previousKeyboard && !allowKeyboard && m_InputHandler != nullptr) { + m_InputHandler->raiseAllKeys(); + } + + const bool previousMouse = m_AllowMouseInput.exchange(allowMouse, std::memory_order_relaxed); + if (previousMouse && !allowMouse && m_InputHandler != nullptr) { + m_InputHandler->raiseAllMouseButtons(); + } + + const bool previousGamepad = m_AllowGamepadInput.exchange(allowGamepad, std::memory_order_relaxed); + if (previousGamepad && !allowGamepad && m_InputHandler != nullptr) { + m_InputHandler->raiseAllGamepadInputs(); + } + + const bool policyChanged = + previousKeyboard != allowKeyboard || + previousMouse != allowMouse || + previousGamepad != allowGamepad; + if (reason == LI_SESSION_INPUT_POLICY_REASON_STREAM_START || + reason == LI_SESSION_INPUT_POLICY_REASON_HOST_ACK || + reason == LI_SESSION_INPUT_POLICY_REASON_HOST_OVERRIDE || + !policyChanged) { + refreshControlPanelOverlay(); + } + else { + notifyInputPermissionState(); + } +} + +void Session::updateEffectiveAudioMuteState() +{ + bool muted = m_ManualAudioMuted.load(std::memory_order_relaxed); + if (!muted && m_Preferences->muteOnFocusLoss && m_Window != nullptr) { + muted = SDL_GetKeyboardFocus() != m_Window; + } + + m_AudioMuted.store(muted, std::memory_order_relaxed); } Session::~Session() @@ -1541,14 +1826,8 @@ void Session::notifyMouseEmulationMode(bool enabled) m_MouseEmulationRefCount += enabled ? 1 : -1; SDL_assert(m_MouseEmulationRefCount >= 0); - // We re-use the status update overlay for mouse mode notification - if (m_MouseEmulationRefCount > 0) { - m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, "Gamepad mouse mode active\nLong press Start to deactivate"); - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); - } - else { - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); - } + showTemporaryStatusOverlay(enabled ? "Gamepad mouse mode enabled" : "Gamepad mouse mode disabled"); + refreshControlPanelOverlay(); } class AsyncConnectionStartThread : public QThread @@ -1747,6 +2026,17 @@ void Session::start() // We're now active s_ActiveSession = this; + m_AllowGamepadInput.store(true, std::memory_order_relaxed); + m_AllowKeyboardInput.store(false, std::memory_order_relaxed); + m_AllowMouseInput.store(false, std::memory_order_relaxed); + m_ManualAudioMuted.store(false, std::memory_order_relaxed); + m_AudioMuted.store(false, std::memory_order_relaxed); + m_AudioVolumeScalar.store(1.0f, std::memory_order_relaxed); + m_ControlPanelVisible.store(true, std::memory_order_relaxed); + m_ConnectionStatus.store(CONN_STATUS_OKAY, std::memory_order_relaxed); + m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); + m_TemporaryStatusOverlayText[0] = '\0'; + // Initialize the gamepad code with our preferences // NB: m_InputHandler must be initialize before starting the connection. m_InputHandler = new SdlInputHandler(*m_Preferences, m_StreamConfig.width, m_StreamConfig.height); @@ -1949,6 +2239,7 @@ void Session::exec() // Toggle the stats overlay if requested by the user m_OverlayManager.setOverlayState(Overlay::OverlayDebug, m_Preferences->showPerformanceOverlay); + refreshControlPanelOverlay(); // Switch to async logging mode when we enter the SDL loop StreamUtils::enterAsyncLoggingMode(); @@ -1956,7 +2247,14 @@ void Session::exec() // Hijack this thread to be the SDL main thread. We have to do this // because we want to suspend all Qt processing until the stream is over. SDL_Event event; + uint32_t nextGamepadPollTime = SDL_GetTicks(); for (;;) { + uint32_t now = SDL_GetTicks(); + if (SDL_TICKS_PASSED(now, nextGamepadPollTime)) { + m_InputHandler->pollForMissingGamepads(); + nextGamepadPollTime = now + 250; + } + #if SDL_VERSION_ATLEAST(2, 0, 18) && !defined(STEAM_LINK) // SDL 2.0.18 has a proper wait event implementation that uses platform // support to block on events rather than polling on Windows, macOS, X11, @@ -1968,6 +2266,7 @@ void Session::exec() // issues that could cause indefinite timeouts, delayed joystick detection, // and other problems. if (!SDL_WaitEventTimeout(&event, 1000)) { + updateEffectiveAudioMuteState(); presence.runCallbacks(); continue; } @@ -1984,10 +2283,14 @@ void Session::exec() // ARM core in the Steam Link, so we will wait 10 ms instead. SDL_Delay(10); #endif + updateEffectiveAudioMuteState(); presence.runCallbacks(); continue; } #endif + + updateEffectiveAudioMuteState(); + switch (event.type) { case SDL_QUIT: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, @@ -2029,6 +2332,21 @@ void Session::exec() m_InputHandler->setAdaptiveTriggers((uint16_t)(uintptr_t)event.user.data1, (DualSenseOutputReport *)event.user.data2); break; + case SDL_CODE_SET_INPUT_POLICY: { + const uintptr_t packed = (uintptr_t)event.user.data2; + const bool allowKeyboard = ((packed >> 24) & 0xFF) != 0; + const bool allowMouse = ((packed >> 16) & 0xFF) != 0; + const bool allowGamepad = ((packed >> 8) & 0xFF) != 0; + const uint8_t reason = (uint8_t)(packed & 0xFF); + applyHostInputPolicy(allowKeyboard, allowMouse, allowGamepad, reason); + break; + } + case SDL_CODE_HIDE_STATUS_OVERLAY: + if ((uint32_t)(uintptr_t)event.user.data1 == m_StatusOverlayGeneration.load(std::memory_order_relaxed)) { + m_TemporaryStatusOverlayText[0] = '\0'; + refreshControlPanelOverlay(); + } + break; default: SDL_assert(false); } @@ -2038,15 +2356,9 @@ void Session::exec() // Early handling of some events switch (event.window.event) { case SDL_WINDOWEVENT_FOCUS_LOST: - if (m_Preferences->muteOnFocusLoss) { - m_AudioMuted = true; - } m_InputHandler->notifyFocusLost(); break; case SDL_WINDOWEVENT_FOCUS_GAINED: - if (m_Preferences->muteOnFocusLoss) { - m_AudioMuted = false; - } m_InputHandler->notifyFocusGained(); break; case SDL_WINDOWEVENT_LEAVE: @@ -2233,6 +2545,7 @@ void Session::exec() // After a window resize, we need to reset the pointer lock region m_InputHandler->updatePointerRegionLock(); + refreshControlPanelOverlay(); SDL_UnlockMutex(m_DecoderLock); break; @@ -2283,6 +2596,9 @@ void Session::exec() case SDL_JOYDEVICEADDED: m_InputHandler->handleJoystickArrivalEvent(&event.jdevice); break; + case SDL_JOYDEVICEREMOVED: + m_InputHandler->handleJoystickRemovalEvent(&event.jdevice); + break; case SDL_FINGERDOWN: case SDL_FINGERMOTION: case SDL_FINGERUP: diff --git a/app/streaming/session.h b/app/streaming/session.h index b4bba36d..6289bf62 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -125,6 +126,47 @@ public: void setShouldExit(bool quitHostApp = false); + bool isGamepadInputAllowed() const + { + return m_AllowGamepadInput.load(std::memory_order_relaxed); + } + + bool isKeyboardInputAllowed() const + { + return m_AllowKeyboardInput.load(std::memory_order_relaxed); + } + + bool isMouseInputAllowed() const + { + return m_AllowMouseInput.load(std::memory_order_relaxed); + } + + bool isKeyboardMouseInputAllowed() const + { + return isKeyboardInputAllowed() && isMouseInputAllowed(); + } + + void setGamepadInputAllowed(bool allowed); + void setKeyboardInputAllowed(bool allowed); + void setMouseInputAllowed(bool allowed); + void setKeyboardMouseInputAllowed(bool allowed); + void toggleGamepadInputAllowed(); + void toggleKeyboardInputAllowed(); + void toggleMouseInputAllowed(); + void toggleKeyboardMouseInputAllowed(); + void toggleControlPanelVisibility(); + void notifyInputPermissionState(); + + float getAudioVolumeScalar() const + { + return m_AudioVolumeScalar.load(std::memory_order_relaxed); + } + + void setAudioVolumeScalar(float scalar); + void adjustAudioVolume(float delta); + void toggleAudioMute(); + void notifyAudioVolumeState(); + signals: void stageStarting(QString stage); @@ -222,6 +264,9 @@ private: static void clSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags, uint8_t typeLeft, uint8_t typeRight, uint8_t *left, uint8_t *right); + static + void clSetInputPolicy(uint8_t allowKeyboard, uint8_t allowMouse, uint8_t allowGamepad, uint8_t reason); + static int arInit(int audioConfiguration, const POPUS_MULTISTREAM_CONFIGURATION opusConfig, @@ -242,6 +287,15 @@ private: static int drSubmitDecodeUnit(PDECODE_UNIT du); + static + Uint32 statusOverlayTimeoutCallback(Uint32 interval, void* param); + + void refreshControlPanelOverlay(); + void showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs = 1500); + void applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool allowGamepad, uint8_t reason); + void sendInputPermissionStateToHost(uint8_t reason); + void updateEffectiveAudioMuteState(); + StreamingPreferences* m_Preferences; bool m_IsFullScreen; SupportedVideoFormatList m_SupportedVideoFormats; // Sorted in order of descending priority @@ -254,7 +308,15 @@ private: IVideoDecoder* m_VideoDecoder; SDL_mutex* m_DecoderLock; bool m_AudioDisabled; - bool m_AudioMuted; + std::atomic m_AudioMuted; + std::atomic m_ManualAudioMuted; + std::atomic m_AudioVolumeScalar; + std::atomic m_AllowGamepadInput; + std::atomic m_AllowKeyboardInput; + std::atomic m_AllowMouseInput; + std::atomic m_ControlPanelVisible; + std::atomic m_ConnectionStatus; + std::atomic m_StatusOverlayGeneration; Uint32 m_FullScreenFlag; QQuickWindow* m_QtWindow; bool m_UnexpectedTermination; @@ -278,6 +340,7 @@ private: OPUS_MULTISTREAM_CONFIGURATION m_OriginalAudioConfig; int m_AudioSampleCount; Uint32 m_DropAudioEndTime; + char m_TemporaryStatusOverlayText[160]; Overlay::OverlayManager m_OverlayManager; diff --git a/app/streaming/video/overlaymanager.cpp b/app/streaming/video/overlaymanager.cpp index 17e5e5c5..77150cc9 100644 --- a/app/streaming/video/overlaymanager.cpp +++ b/app/streaming/video/overlaymanager.cpp @@ -12,8 +12,8 @@ OverlayManager::OverlayManager() : m_Overlays[OverlayType::OverlayDebug].color = {0xD0, 0xD0, 0x00, 0xFF}; m_Overlays[OverlayType::OverlayDebug].fontSize = 20; - m_Overlays[OverlayType::OverlayStatusUpdate].color = {0xCC, 0x00, 0x00, 0xFF}; - m_Overlays[OverlayType::OverlayStatusUpdate].fontSize = 36; + m_Overlays[OverlayType::OverlayStatusUpdate].color = {0xF0, 0xF0, 0xF0, 0xFF}; + m_Overlays[OverlayType::OverlayStatusUpdate].fontSize = 24; // While TTF will usually not be initialized here, it is valid for that not to // be the case, since Session destruction is deferred and could overlap with @@ -146,16 +146,45 @@ void OverlayManager::notifyOverlayUpdated(OverlayType type) } } + SDL_Surface* newSurface = nullptr; + if (m_Overlays[type].enabled) { + // The _Wrapped variant is required for line breaks to work + SDL_Surface* textSurface = TTF_RenderText_Blended_Wrapped(m_Overlays[type].font, + m_Overlays[type].text, + m_Overlays[type].color, + 1024); + if (textSurface != nullptr && type == OverlayStatusUpdate) { + constexpr int kHorizontalPadding = 18; + constexpr int kVerticalPadding = 12; + + SDL_Surface* panelSurface = SDL_CreateRGBSurfaceWithFormat(0, + textSurface->w + (kHorizontalPadding * 2), + textSurface->h + (kVerticalPadding * 2), + 32, + SDL_PIXELFORMAT_ARGB8888); + if (panelSurface != nullptr) { + SDL_FillRect(panelSurface, + nullptr, + SDL_MapRGBA(panelSurface->format, 0x00, 0x00, 0x00, 0xA0)); + + SDL_Rect destination = {kHorizontalPadding, kVerticalPadding, textSurface->w, textSurface->h}; + SDL_SetSurfaceBlendMode(textSurface, SDL_BLENDMODE_BLEND); + SDL_BlitSurface(textSurface, nullptr, panelSurface, &destination); + SDL_FreeSurface(textSurface); + newSurface = panelSurface; + } + else { + newSurface = textSurface; + } + } + else { + newSurface = textSurface; + } + } + // Exchange the old surface with the new one - SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicSetPtr( - (void**)&m_Overlays[type].surface, - m_Overlays[type].enabled ? - // The _Wrapped variant is required for line breaks to work - TTF_RenderText_Blended_Wrapped(m_Overlays[type].font, - m_Overlays[type].text, - m_Overlays[type].color, - 1024) - : nullptr); + SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicSetPtr((void**)&m_Overlays[type].surface, + newSurface); // Notify the renderer m_Renderer->notifyOverlayUpdated(type); diff --git a/moonlight-common-c/moonlight-common-c b/moonlight-common-c/moonlight-common-c index 6250fa29..611a2e7f 160000 --- a/moonlight-common-c/moonlight-common-c +++ b/moonlight-common-c/moonlight-common-c @@ -1 +1 @@ -Subproject commit 6250fa29ee87873716045e3b64f1f229374324e8 +Subproject commit 611a2e7f8f6583d6d6aad30f0c8a02d6c07ab085 diff --git a/scripts/bootstrap-toolbox-build.sh b/scripts/bootstrap-toolbox-build.sh new file mode 100755 index 00000000..d3f546c9 --- /dev/null +++ b/scripts/bootstrap-toolbox-build.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +JOBS="${JOBS:-$(nproc)}" +SKIP_BUILD=0 + +for arg in "$@"; do + case "$arg" in + --skip-build) + SKIP_BUILD=1 + ;; + --jobs=*) + JOBS="${arg#*=}" + ;; + *) + echo "Unknown argument: $arg" + echo "Usage: $0 [--skip-build] [--jobs=N]" + exit 1 + ;; + esac +done + +if [[ ! -f /run/.containerenv ]]; then + echo "Run this script inside your toolbox container." + exit 1 +fi + +if ! command -v dnf >/dev/null 2>&1; then + echo "This script expects a Fedora/RHEL-like toolbox with dnf." + exit 1 +fi + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +FEDORA_VER="$(rpm -E %fedora)" + +if ! rpm -q rpmfusion-free-release >/dev/null 2>&1; then + sudo dnf -y install "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${FEDORA_VER}.noarch.rpm" +fi + +if ! rpm -q rpmfusion-nonfree-release >/dev/null 2>&1; then + sudo dnf -y install "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${FEDORA_VER}.noarch.rpm" +fi + +sudo dnf -y install \ + ffmpeg ffmpeg-devel \ + git make gcc gcc-c++ pkgconf-pkg-config \ + qt6-qtbase-devel qt6-qtdeclarative-devel qt6-qtquickcontrols2-devel qt6-qtsvg-devel qt6-qtwayland-devel \ + SDL2-devel SDL2_ttf-devel \ + opus-devel openssl-devel \ + libva-utils libva-devel libdrm-devel mesa-libEGL-devel mesa-libGL-devel \ + libX11-devel libXext-devel libXrandr-devel libXfixes-devel libXi-devel libXcursor-devel + +if rpm -q libva-intel-media-driver >/dev/null 2>&1; then + sudo dnf -y swap libva-intel-media-driver intel-media-driver --allowerasing +elif ! rpm -q intel-media-driver >/dev/null 2>&1; then + sudo dnf -y install intel-media-driver +fi + +git submodule update --init --recursive + +if command -v vainfo >/dev/null 2>&1; then + if ! vainfo 2>/dev/null | grep -q 'VAProfileHEVCMain'; then + echo "Warning: HEVC decode profile not detected in VAAPI." + fi + if ! vainfo 2>/dev/null | grep -q 'VAProfileH264High'; then + echo "Warning: H264 decode profile not detected in VAAPI." + fi +fi + +rm -rf Makefile */Makefile */Makefile.* app/release app/debug +qmake6 moonlight-qt.pro + +if [[ "$SKIP_BUILD" -eq 0 ]]; then + make release -j"$JOBS" +fi + +echo "Done."