From 97c7b8bf147df936fa228665c71c848a54e29d6d Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 11 Feb 2026 11:40:41 -0700 Subject: [PATCH 01/21] Build toolbox --- scripts/bootstrap-toolbox-build.sh | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 scripts/bootstrap-toolbox-build.sh 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." From f426032b1b44d004efaf950879afee88246bd9d3 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 11 Feb 2026 17:18:53 -0700 Subject: [PATCH 02/21] Ignore local qmake and build outputs --- .gitignore | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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 From f78a63ab753693258bc8f6f7e3adf7f05ae9fb93 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 11 Feb 2026 17:18:53 -0700 Subject: [PATCH 03/21] Add runtime input toggles and gated input sending --- app/streaming/input/abstouch.cpp | 31 ++++-- app/streaming/input/gamepad.cpp | 165 ++++++++++++++++++++++++------- app/streaming/input/input.cpp | 10 ++ app/streaming/input/input.h | 2 + app/streaming/input/keyboard.cpp | 38 +++++-- app/streaming/input/mouse.cpp | 41 ++++++-- app/streaming/input/reltouch.cpp | 33 +++++-- app/streaming/session.cpp | 41 ++++++++ app/streaming/session.h | 19 ++++ 9 files changed, 312 insertions(+), 68 deletions(-) diff --git a/app/streaming/input/abstouch.cpp b/app/streaming/input/abstouch.cpp index 0fe6dac6..9e8b51a9 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->isKeyboardMouseInputAllowed(); +} + // 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..d5171f22 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -6,6 +6,18 @@ #include +static bool isGamepadInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isGamepadInputAllowed(); +} + +static bool isKeyboardMouseInputAllowed() +{ + auto session = Session::get(); + return session == nullptr || session->isKeyboardMouseInputAllowed(); +} + // How long the Start button must be pressed to toggle mouse emulation #define MOUSE_EMULATION_LONG_PRESS_TIME 750 @@ -101,15 +113,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 +164,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 +197,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) && isKeyboardMouseInputAllowed()) { LiSendMouseMoveEvent((short)deltaX, (short)deltaY); } @@ -291,31 +307,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 (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_B) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_X) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); + } } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) { - LiSendScrollEvent(1); + if (isKeyboardMouseInputAllowed()) { + LiSendScrollEvent(1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { - LiSendScrollEvent(-1); + if (isKeyboardMouseInputAllowed()) { + LiSendScrollEvent(-1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) { - LiSendHScrollEvent(1); + if (isKeyboardMouseInputAllowed()) { + LiSendHScrollEvent(1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) { - LiSendHScrollEvent(-1); + if (isKeyboardMouseInputAllowed()) { + LiSendHScrollEvent(-1); + } } } } @@ -346,19 +380,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 (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_B) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + } } else if (event->button == SDL_CONTROLLER_BUTTON_X) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); + } } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); + } } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); + if (isKeyboardMouseInputAllowed()) { + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); + } } } } @@ -375,8 +419,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 +436,41 @@ 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 | Y_FLAG)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected keyboard/mouse input toggle gamepad combo"); + + Session::get()->toggleKeyboardMouseInputAllowed(); + + 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; } @@ -418,7 +497,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 +510,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; } @@ -460,7 +543,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 @@ -708,7 +793,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 +837,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)); diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 982eaaa9..844e0ab4 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -106,6 +106,16 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V; m_SpecialKeyCombos[KeyComboPasteText].enabled = true; + m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].keyCombo = KeyComboToggleKeyboardMouseInput; + m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].keyCode = SDLK_k; + m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].scanCode = SDL_SCANCODE_K; + m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].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[KeyComboTogglePointerRegionLock].keyCombo = KeyComboTogglePointerRegionLock; m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].keyCode = SDLK_l; m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].scanCode = SDL_SCANCODE_L; diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 20bbbc33..91a020ce 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -165,6 +165,8 @@ private: KeyComboToggleCursorHide, KeyComboToggleMinimize, KeyComboPasteText, + KeyComboToggleKeyboardMouseInput, + KeyComboToggleGamepadInput, KeyComboTogglePointerRegionLock, KeyComboQuitAndExit, KeyComboMax diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index 273816f4..819837c0 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->isKeyboardMouseInputAllowed(); +} + void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) { switch (combo) { @@ -114,7 +120,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 +134,22 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) break; } + case KeyComboToggleKeyboardMouseInput: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Detected keyboard/mouse input toggle combo"); + if (auto session = Session::get(); session != nullptr) { + session->toggleKeyboardMouseInputAllowed(); + } + 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 KeyComboTogglePointerRegionLock: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected pointer region lock toggle combo"); @@ -457,9 +481,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..c86eae54 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->isKeyboardMouseInputAllowed(); +} + 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..1649f1c1 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->isKeyboardMouseInputAllowed(); +} + // 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/session.cpp b/app/streaming/session.cpp index ad3741b0..c314ae83 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -556,6 +556,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_VideoDecoder(nullptr), m_DecoderLock(SDL_CreateMutex()), m_AudioMuted(false), + m_AllowGamepadInput(true), + m_AllowKeyboardMouseInput(true), m_QtWindow(nullptr), m_UnexpectedTermination(true), // Failure prior to streaming is unexpected m_InputHandler(nullptr), @@ -571,6 +573,45 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere { } +void Session::setGamepadInputAllowed(bool allowed) +{ + m_AllowGamepadInput.store(allowed, std::memory_order_relaxed); + notifyInputPermissionState(); +} + +void Session::setKeyboardMouseInputAllowed(bool allowed) +{ + bool previous = m_AllowKeyboardMouseInput.exchange(allowed, std::memory_order_relaxed); + if (previous && !allowed && m_InputHandler != nullptr) { + m_InputHandler->raiseAllKeys(); + } + + notifyInputPermissionState(); +} + +void Session::toggleGamepadInputAllowed() +{ + setGamepadInputAllowed(!isGamepadInputAllowed()); +} + +void Session::toggleKeyboardMouseInputAllowed() +{ + setKeyboardMouseInputAllowed(!isKeyboardMouseInputAllowed()); +} + +void Session::notifyInputPermissionState() +{ + char buffer[96]; + SDL_snprintf(buffer, + sizeof(buffer), + "Keyboard/Mouse: %s\nGamepad: %s", + isKeyboardMouseInputAllowed() ? "ON" : "OFF", + isGamepadInputAllowed() ? "ON" : "OFF"); + + m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, buffer); + m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); +} + Session::~Session() { // NB: This may not get destroyed for a long time! Don't put any non-trivial cleanup here. diff --git a/app/streaming/session.h b/app/streaming/session.h index b4bba36d..d232bdbc 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -125,6 +126,22 @@ public: void setShouldExit(bool quitHostApp = false); + bool isGamepadInputAllowed() const + { + return m_AllowGamepadInput.load(std::memory_order_relaxed); + } + + bool isKeyboardMouseInputAllowed() const + { + return m_AllowKeyboardMouseInput.load(std::memory_order_relaxed); + } + + void setGamepadInputAllowed(bool allowed); + void setKeyboardMouseInputAllowed(bool allowed); + void toggleGamepadInputAllowed(); + void toggleKeyboardMouseInputAllowed(); + void notifyInputPermissionState(); + signals: void stageStarting(QString stage); @@ -255,6 +272,8 @@ private: SDL_mutex* m_DecoderLock; bool m_AudioDisabled; bool m_AudioMuted; + std::atomic m_AllowGamepadInput; + std::atomic m_AllowKeyboardMouseInput; Uint32 m_FullScreenFlag; QQuickWindow* m_QtWindow; bool m_UnexpectedTermination; From 9e52a4200b9ee39d25caa5c2f3cbd7746dff934d Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 11 Feb 2026 21:20:48 -0700 Subject: [PATCH 04/21] Phase 4 volume --- app/streaming/audio/audio.cpp | 24 ++++++- app/streaming/input/input.cpp | 15 +++++ app/streaming/input/input.h | 3 + app/streaming/input/keyboard.cpp | 24 +++++++ app/streaming/session.cpp | 104 ++++++++++++++++++++++++++++--- app/streaming/session.h | 21 ++++++- 6 files changed, 182 insertions(+), 9 deletions(-) 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/input.cpp b/app/streaming/input/input.cpp index 844e0ab4..ddeebec4 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -116,6 +116,21 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i 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; diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 91a020ce..050f150c 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -167,6 +167,9 @@ private: KeyComboPasteText, KeyComboToggleKeyboardMouseInput, KeyComboToggleGamepadInput, + KeyComboVolumeUp, + KeyComboVolumeDown, + KeyComboToggleMute, KeyComboTogglePointerRegionLock, KeyComboQuitAndExit, KeyComboMax diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index 819837c0..1f8a8214 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -150,6 +150,30 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) } 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"); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index c314ae83..aa18860f 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -28,6 +28,7 @@ #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 #include @@ -187,12 +188,14 @@ void Session::clConnectionStatusUpdate(int connectionStatus) switch (connectionStatus) { case CONN_STATUS_POOR: + s_ActiveSession->m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); 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_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); break; } @@ -556,8 +559,11 @@ 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_AllowKeyboardMouseInput(true), + m_StatusOverlayGeneration(0), m_QtWindow(nullptr), m_UnexpectedTermination(true), // Failure prior to streaming is unexpected m_InputHandler(nullptr), @@ -579,6 +585,43 @@ void Session::setGamepadInputAllowed(bool allowed) 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) { bool previous = m_AllowKeyboardMouseInput.exchange(allowed, std::memory_order_relaxed); @@ -608,8 +651,41 @@ void Session::notifyInputPermissionState() isKeyboardMouseInputAllowed() ? "ON" : "OFF", isGamepadInputAllowed() ? "ON" : "OFF"); - m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, buffer); + showTemporaryStatusOverlay(buffer); +} + +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::showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs) +{ + if (m_MouseEmulationRefCount > 0) { + return; + } + + m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, text); m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); + + const uint32_t generation = m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed) + 1; + SDL_AddTimer(timeoutMs, statusOverlayTimeoutCallback, (void*)(uintptr_t)generation); +} + +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() @@ -1584,10 +1660,12 @@ void Session::notifyMouseEmulationMode(bool enabled) // We re-use the status update overlay for mouse mode notification if (m_MouseEmulationRefCount > 0) { + m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, "Gamepad mouse mode active\nLong press Start to deactivate"); m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); } else { + m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); } } @@ -1788,6 +1866,13 @@ void Session::start() // We're now active s_ActiveSession = this; + m_AllowGamepadInput.store(true, std::memory_order_relaxed); + m_AllowKeyboardMouseInput.store(true, 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_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); + // 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); @@ -2009,6 +2094,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; } @@ -2025,10 +2111,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, @@ -2070,6 +2160,12 @@ void Session::exec() m_InputHandler->setAdaptiveTriggers((uint16_t)(uintptr_t)event.user.data1, (DualSenseOutputReport *)event.user.data2); break; + case SDL_CODE_HIDE_STATUS_OVERLAY: + if ((uint32_t)(uintptr_t)event.user.data1 == m_StatusOverlayGeneration.load(std::memory_order_relaxed) && + m_MouseEmulationRefCount == 0) { + m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); + } + break; default: SDL_assert(false); } @@ -2079,15 +2175,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: diff --git a/app/streaming/session.h b/app/streaming/session.h index d232bdbc..c0c01815 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -142,6 +142,16 @@ public: void toggleKeyboardMouseInputAllowed(); 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); @@ -259,6 +269,12 @@ private: static int drSubmitDecodeUnit(PDECODE_UNIT du); + static + Uint32 statusOverlayTimeoutCallback(Uint32 interval, void* param); + + void showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs = 1500); + void updateEffectiveAudioMuteState(); + StreamingPreferences* m_Preferences; bool m_IsFullScreen; SupportedVideoFormatList m_SupportedVideoFormats; // Sorted in order of descending priority @@ -271,9 +287,12 @@ 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_AllowKeyboardMouseInput; + std::atomic m_StatusOverlayGeneration; Uint32 m_FullScreenFlag; QQuickWindow* m_QtWindow; bool m_UnexpectedTermination; From 7e9a4add090c8f007fc4ffaa22593e910faa552f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 00:38:05 -0700 Subject: [PATCH 05/21] Finalize session input policy host sync --- app/streaming/input/input.cpp | 9 ++++ app/streaming/input/input.h | 2 + app/streaming/session.cpp | 66 ++++++++++++++++++++++++++- app/streaming/session.h | 5 ++ moonlight-common-c/moonlight-common-c | 2 +- 5 files changed, 81 insertions(+), 3 deletions(-) diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index ddeebec4..a2a109e7 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -293,6 +293,15 @@ 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::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 050f150c..753ff275 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -134,6 +134,8 @@ public: void raiseAllKeys(); + void raiseAllMouseButtons(); + void notifyMouseLeave(); void notifyFocusLost(); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index aa18860f..69bf4d02 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -29,6 +29,7 @@ #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 @@ -61,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; @@ -277,6 +279,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, @@ -562,7 +578,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_ManualAudioMuted(false), m_AudioVolumeScalar(1.0f), m_AllowGamepadInput(true), - m_AllowKeyboardMouseInput(true), + m_AllowKeyboardMouseInput(false), m_StatusOverlayGeneration(0), m_QtWindow(nullptr), m_UnexpectedTermination(true), // Failure prior to streaming is unexpected @@ -582,6 +598,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere void Session::setGamepadInputAllowed(bool allowed) { m_AllowGamepadInput.store(allowed, std::memory_order_relaxed); + sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_USER_TOGGLE); notifyInputPermissionState(); } @@ -627,8 +644,10 @@ void Session::setKeyboardMouseInputAllowed(bool allowed) bool previous = m_AllowKeyboardMouseInput.exchange(allowed, std::memory_order_relaxed); if (previous && !allowed && m_InputHandler != nullptr) { m_InputHandler->raiseAllKeys(); + m_InputHandler->raiseAllMouseButtons(); } + sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_USER_TOGGLE); notifyInputPermissionState(); } @@ -654,6 +673,22 @@ void Session::notifyInputPermissionState() showTemporaryStatusOverlay(buffer); } +void Session::sendInputPermissionStateToHost(uint8_t reason) +{ + const bool allowKeyboardMouse = isKeyboardMouseInputAllowed(); + const bool allowGamepad = isGamepadInputAllowed(); + const int err = LiSendSessionInputPolicy(allowKeyboardMouse, + allowKeyboardMouse, + 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 = {}; @@ -678,6 +713,22 @@ void Session::showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs) SDL_AddTimer(timeoutMs, statusOverlayTimeoutCallback, (void*)(uintptr_t)generation); } +void Session::applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool allowGamepad, uint8_t reason) +{ + const bool allowKeyboardMouse = allowKeyboard && allowMouse; + const bool previousKeyboardMouse = m_AllowKeyboardMouseInput.exchange(allowKeyboardMouse, std::memory_order_relaxed); + if (previousKeyboardMouse && !allowKeyboardMouse && m_InputHandler != nullptr) { + m_InputHandler->raiseAllKeys(); + m_InputHandler->raiseAllMouseButtons(); + } + + m_AllowGamepadInput.store(allowGamepad, std::memory_order_relaxed); + + if (reason != LI_SESSION_INPUT_POLICY_REASON_STREAM_START) { + notifyInputPermissionState(); + } +} + void Session::updateEffectiveAudioMuteState() { bool muted = m_ManualAudioMuted.load(std::memory_order_relaxed); @@ -1824,6 +1875,8 @@ bool Session::startConnectionAsync() return false; } + sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_STREAM_START); + emit connectionStarted(); return true; } @@ -2160,6 +2213,15 @@ 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_MouseEmulationRefCount == 0) { diff --git a/app/streaming/session.h b/app/streaming/session.h index c0c01815..8e5bd35f 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -249,6 +249,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, @@ -273,6 +276,8 @@ private: Uint32 statusOverlayTimeoutCallback(Uint32 interval, void* param); 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; 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 From 7ddadd21d403b79e85dbbce7651d7c18d044da14 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 00:49:59 -0700 Subject: [PATCH 06/21] Point moonlight-common-c submodule to self-hosted remote --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8a7be971a4007cd72476abc43d9ab635c0a06481 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 01:15:03 -0700 Subject: [PATCH 07/21] Make in-stream control panel persistent and readable --- app/streaming/session.cpp | 132 +++++++++++++++++-------- app/streaming/session.h | 5 + app/streaming/video/overlaymanager.cpp | 51 +++++++--- 3 files changed, 138 insertions(+), 50 deletions(-) diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 69bf4d02..57ba29b1 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -178,29 +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_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - 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_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - 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) @@ -579,6 +558,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_AudioVolumeScalar(1.0f), m_AllowGamepadInput(true), m_AllowKeyboardMouseInput(false), + m_ControlPanelVisible(true), + m_ConnectionStatus(CONN_STATUS_OKAY), m_StatusOverlayGeneration(0), m_QtWindow(nullptr), m_UnexpectedTermination(true), // Failure prior to streaming is unexpected @@ -593,6 +574,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_AudioSampleCount(0), m_DropAudioEndTime(0) { + m_TemporaryStatusOverlayText[0] = '\0'; } void Session::setGamepadInputAllowed(bool allowed) @@ -661,6 +643,13 @@ 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[96]; @@ -700,14 +689,80 @@ Uint32 Session::statusOverlayTimeoutCallback(Uint32, void* param) return 0; } -void Session::showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs) +void Session::refreshControlPanelOverlay() { - if (m_MouseEmulationRefCount > 0) { + 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; } - m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, text); + 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" + "KB/M (Ctrl+Alt+Shift+K): %s\n" + "Pad (Ctrl+Alt+Shift+G): %s\n" + "Vol (U/J, mute N): %s\n" + "UI (Ctrl+Alt+Shift+P / Select+L1+R1+B): ON", + isKeyboardMouseInputAllowed() ? "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); @@ -727,6 +782,9 @@ void Session::applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool all if (reason != LI_SESSION_INPUT_POLICY_REASON_STREAM_START) { notifyInputPermissionState(); } + else { + refreshControlPanelOverlay(); + } } void Session::updateEffectiveAudioMuteState() @@ -1709,16 +1767,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_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, "Gamepad mouse mode active\nLong press Start to deactivate"); - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); - } - else { - m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); - } + showTemporaryStatusOverlay(enabled ? "Gamepad mouse mode enabled" : "Gamepad mouse mode disabled"); + refreshControlPanelOverlay(); } class AsyncConnectionStartThread : public QThread @@ -1924,7 +1974,10 @@ void Session::start() 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. @@ -2128,6 +2181,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(); @@ -2223,9 +2277,9 @@ void Session::exec() break; } case SDL_CODE_HIDE_STATUS_OVERLAY: - if ((uint32_t)(uintptr_t)event.user.data1 == m_StatusOverlayGeneration.load(std::memory_order_relaxed) && - m_MouseEmulationRefCount == 0) { - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); + if ((uint32_t)(uintptr_t)event.user.data1 == m_StatusOverlayGeneration.load(std::memory_order_relaxed)) { + m_TemporaryStatusOverlayText[0] = '\0'; + refreshControlPanelOverlay(); } break; default: diff --git a/app/streaming/session.h b/app/streaming/session.h index 8e5bd35f..13eca494 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -140,6 +140,7 @@ public: void setKeyboardMouseInputAllowed(bool allowed); void toggleGamepadInputAllowed(); void toggleKeyboardMouseInputAllowed(); + void toggleControlPanelVisibility(); void notifyInputPermissionState(); float getAudioVolumeScalar() const @@ -275,6 +276,7 @@ private: 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); @@ -297,6 +299,8 @@ private: std::atomic m_AudioVolumeScalar; std::atomic m_AllowGamepadInput; std::atomic m_AllowKeyboardMouseInput; + std::atomic m_ControlPanelVisible; + std::atomic m_ConnectionStatus; std::atomic m_StatusOverlayGeneration; Uint32 m_FullScreenFlag; QQuickWindow* m_QtWindow; @@ -321,6 +325,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); From 3767a9eac3482882d915c7b91db5d46348d8e609 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 01:15:12 -0700 Subject: [PATCH 08/21] Add keyboard and gamepad shortcuts for control panel --- app/streaming/input/gamepad.cpp | 13 +++++++++++++ app/streaming/input/input.cpp | 5 +++++ app/streaming/input/input.h | 1 + app/streaming/input/keyboard.cpp | 8 ++++++++ 4 files changed, 27 insertions(+) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index d5171f22..94625f6c 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -443,6 +443,19 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve 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/mouse input toggle gamepad combo"); diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index a2a109e7..819519a0 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -86,6 +86,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; diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 753ff275..780e0383 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -163,6 +163,7 @@ private: KeyComboUngrabInput, KeyComboToggleFullScreen, KeyComboToggleStatsOverlay, + KeyComboToggleControlPanel, KeyComboToggleMouseMode, KeyComboToggleCursorHide, KeyComboToggleMinimize, diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index 1f8a8214..7d5a03c4 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -64,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"); From e88be5dce229debaeedfaa49ef817fdc3cec57eb Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 01:36:36 -0700 Subject: [PATCH 09/21] Align input policy startup flow and fix gamepad reconnect handling --- app/streaming/input/gamepad.cpp | 14 +++++++++++++ app/streaming/input/input.cpp | 35 +++++++++++++++++++++++++++++++++ app/streaming/input/input.h | 4 ++++ app/streaming/session.cpp | 16 +++++++++++---- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 94625f6c..86ff3996 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -890,6 +890,20 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) } } +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) { // Make sure the controller number is within our supported count diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 819519a0..c5f8f95b 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -307,6 +307,41 @@ void SdlInputHandler::raiseAllMouseButtons() 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 780e0383..4bcdd2de 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -116,6 +116,8 @@ public: void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); + void handleJoystickRemovalEvent(SDL_JoyDeviceEvent* event); + void sendText(QString& string); void rumble(uint16_t controllerNumber, uint16_t lowFreqMotor, uint16_t highFreqMotor); @@ -136,6 +138,8 @@ public: void raiseAllMouseButtons(); + void raiseAllGamepadInputs(); + void notifyMouseLeave(); void notifyFocusLost(); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 57ba29b1..c2a6b380 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -579,6 +579,10 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere 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(); @@ -777,7 +781,10 @@ void Session::applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool all m_InputHandler->raiseAllMouseButtons(); } - m_AllowGamepadInput.store(allowGamepad, std::memory_order_relaxed); + const bool previousGamepad = m_AllowGamepadInput.exchange(allowGamepad, std::memory_order_relaxed); + if (previousGamepad && !allowGamepad && m_InputHandler != nullptr) { + m_InputHandler->raiseAllGamepadInputs(); + } if (reason != LI_SESSION_INPUT_POLICY_REASON_STREAM_START) { notifyInputPermissionState(); @@ -1925,8 +1932,6 @@ bool Session::startConnectionAsync() return false; } - sendInputPermissionStateToHost(LI_SESSION_INPUT_POLICY_REASON_STREAM_START); - emit connectionStarted(); return true; } @@ -1970,7 +1975,7 @@ void Session::start() s_ActiveSession = this; m_AllowGamepadInput.store(true, std::memory_order_relaxed); - m_AllowKeyboardMouseInput.store(true, std::memory_order_relaxed); + m_AllowKeyboardMouseInput.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); @@ -2530,6 +2535,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: From ca41cf32cc8ddb683bca827db1ae543394c724bd Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 01:44:04 -0700 Subject: [PATCH 10/21] Clean up stale detached gamepads before hotplug reattach --- app/streaming/input/gamepad.cpp | 21 +++++++++++++++++++++ app/streaming/input/input.h | 2 ++ 2 files changed, 23 insertions(+) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 86ff3996..70854dd1 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -63,6 +63,25 @@ SdlInputHandler::findStateForGamepad(SDL_JoystickID id) return nullptr; } +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); @@ -582,6 +601,8 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve GamepadState* state; if (event->type == SDL_CONTROLLERDEVICEADDED) { + cleanupDetachedGamepads(); + int i; const char* name; SDL_GameController* controller; diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 4bcdd2de..2cab20f7 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -185,6 +185,8 @@ private: GamepadState* findStateForGamepad(SDL_JoystickID id); + void cleanupDetachedGamepads(); + void sendGamepadState(GamepadState* state); void sendGamepadBatteryState(GamepadState* state, SDL_JoystickPowerLevel level); From cf652190a347c049984c33c0a8bbcfef26ea9490 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 01:52:03 -0700 Subject: [PATCH 11/21] Handle joystick hotplug fallback and avoid stale gamepad matches --- app/streaming/input/gamepad.cpp | 60 +++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 70854dd1..d6a05902 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -52,7 +52,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]; } @@ -618,15 +618,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) { + if (m_GamepadState[i].controller != nullptr && m_GamepadState[i].jsId == jsId) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Received duplicate add event for controller index: %d", - event->which); + "Received duplicate add event for joystick instance ID: %d", + jsId); SDL_GameControllerClose(controller); return; } @@ -682,7 +684,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) @@ -886,28 +888,34 @@ 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); + 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, - "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()); - } + "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()); } } From e00558780649b7082eba1161ca6a5f50ed855873 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 02:00:45 -0700 Subject: [PATCH 12/21] Recover gamepad state when add events are missed --- app/streaming/input/gamepad.cpp | 44 +++++++++++++++++++++++++++++---- app/streaming/input/input.h | 3 +++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index d6a05902..2a4038a7 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -63,6 +63,40 @@ 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::cleanupDetachedGamepads() { for (int i = 0; i < MAX_GAMEPADS; i++) { @@ -226,7 +260,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; } @@ -296,7 +330,7 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve return; } - GamepadState* state = findStateForGamepad(event->which); + GamepadState* state = ensureStateForGamepad(event->which); if (state == NULL) { return; } @@ -516,7 +550,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; } @@ -555,7 +589,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; } @@ -586,7 +620,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; } diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 2cab20f7..c306441c 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -185,6 +185,9 @@ private: GamepadState* findStateForGamepad(SDL_JoystickID id); + GamepadState* + ensureStateForGamepad(SDL_JoystickID id); + void cleanupDetachedGamepads(); void sendGamepadState(GamepadState* state); From b03a4341e3144851d953eb367025f83d7fef51b5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 02:06:12 -0700 Subject: [PATCH 13/21] Poll SDL device list to recover missing hotplug events --- app/streaming/input/gamepad.cpp | 27 +++++++++++++++++++++++++++ app/streaming/input/input.h | 2 ++ app/streaming/session.cpp | 1 + 3 files changed, 30 insertions(+) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 2a4038a7..375bc5ac 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -97,6 +97,33 @@ SdlInputHandler::ensureStateForGamepad(SDL_JoystickID id) return findStateForGamepad(id); } +void SdlInputHandler::pollForMissingGamepads() +{ + cleanupDetachedGamepads(); + + const int joystickCount = SDL_NumJoysticks(); + 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); + } +} + void SdlInputHandler::cleanupDetachedGamepads() { for (int i = 0; i < MAX_GAMEPADS; i++) { diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index c306441c..b0890b31 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -118,6 +118,8 @@ public: void handleJoystickRemovalEvent(SDL_JoyDeviceEvent* event); + void pollForMissingGamepads(); + void sendText(QString& string); void rumble(uint16_t controllerNumber, uint16_t lowFreqMotor, uint16_t highFreqMotor); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index c2a6b380..6ddc736a 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -2206,6 +2206,7 @@ void Session::exec() // issues that could cause indefinite timeouts, delayed joystick detection, // and other problems. if (!SDL_WaitEventTimeout(&event, 1000)) { + m_InputHandler->pollForMissingGamepads(); updateEffectiveAudioMuteState(); presence.runCallbacks(); continue; From bcb721bca314e16e4d3be3f479615a0ea2bdc23f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 02:08:56 -0700 Subject: [PATCH 14/21] Poll gamepad recovery even when SDL event queue is busy --- app/streaming/session.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 6ddc736a..7d27e095 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -2194,7 +2194,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, @@ -2206,7 +2213,6 @@ void Session::exec() // issues that could cause indefinite timeouts, delayed joystick detection, // and other problems. if (!SDL_WaitEventTimeout(&event, 1000)) { - m_InputHandler->pollForMissingGamepads(); updateEffectiveAudioMuteState(); presence.runCallbacks(); continue; From 6742e9153ca74bfafa67fbc50d680c3703b7c35a Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 02:10:03 -0700 Subject: [PATCH 15/21] Force SDL joystick refresh during reconnect recovery poll --- app/streaming/input/gamepad.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 375bc5ac..e7f70847 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -99,6 +99,9 @@ SdlInputHandler::ensureStateForGamepad(SDL_JoystickID id) void SdlInputHandler::pollForMissingGamepads() { + SDL_JoystickUpdate(); + SDL_GameControllerUpdate(); + cleanupDetachedGamepads(); const int joystickCount = SDL_NumJoysticks(); From 5f41d9a4c3f84195b8a85e9cbccd57982a72cd24 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 07:38:51 -0700 Subject: [PATCH 16/21] Recover gamepad reconnect by forcing SDL re-enumeration --- app/streaming/input/gamepad.cpp | 109 ++++++++++++++++++++++++++++---- app/streaming/input/input.cpp | 1 + app/streaming/input/input.h | 1 + 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index e7f70847..deb8b4e1 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -24,6 +24,8 @@ static bool isKeyboardMouseInputAllowed() // How long between polling the gamepad to send virtual mouse input #define MOUSE_EMULATION_POLLING_INTERVAL 50 +#define HOTPLUG_REENUMERATION_INTERVAL_MS 2000 + // Determines how fast the mouse will move each interval #define MOUSE_EMULATION_MOTION_MULTIPLIER 4 @@ -99,32 +101,117 @@ SdlInputHandler::ensureStateForGamepad(SDL_JoystickID id) void SdlInputHandler::pollForMissingGamepads() { + static uint32_t s_LastForcedReenumerationTick = 0; + SDL_JoystickUpdate(); SDL_GameControllerUpdate(); - cleanupDetachedGamepads(); + auto recoverUntrackedGamepads = [this](int joystickCount) { + for (int deviceIndex = 0; deviceIndex < joystickCount; deviceIndex++) { + if (!SDL_IsGameController(deviceIndex)) { + continue; + } - const int joystickCount = SDL_NumJoysticks(); - for (int deviceIndex = 0; deviceIndex < joystickCount; deviceIndex++) { - if (!SDL_IsGameController(deviceIndex)) { + 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; + } + + for (int i = 0; i < MAX_GAMEPADS; i++) { + GamepadState* state = &m_GamepadState[i]; + if (state->controller == nullptr) { continue; } - SDL_JoystickID jsId = SDL_JoystickGetDeviceInstanceID(deviceIndex); - if (jsId < 0 || findStateForGamepad(jsId) != nullptr) { + bool presentInDeviceList = false; + for (int deviceIndex = 0; deviceIndex < joystickCount; deviceIndex++) { + if (SDL_JoystickGetDeviceInstanceID(deviceIndex) == state->jsId) { + presentInDeviceList = true; + break; + } + } + + if (presentInDeviceList) { continue; } SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Polling recovered missing gamepad add event (device index: %d, instance ID: %d)", - deviceIndex, - jsId); + "Polling detected disconnected gamepad instance %d in slot %d; cleaning up", + state->jsId, + i); SDL_ControllerDeviceEvent controllerEvent = {}; - controllerEvent.type = SDL_CONTROLLERDEVICEADDED; - controllerEvent.which = deviceIndex; + controllerEvent.type = SDL_CONTROLLERDEVICEREMOVED; + controllerEvent.which = state->jsId; handleControllerDeviceEvent(&controllerEvent); } + + recoverUntrackedGamepads(joystickCount); + + if (joystickCount == 0) { + uint32_t now = SDL_GetTicks(); + if (SDL_TICKS_PASSED(now, s_LastForcedReenumerationTick + HOTPLUG_REENUMERATION_INTERVAL_MS)) { + s_LastForcedReenumerationTick = now; + + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "No joysticks visible; forcing SDL joystick/gamecontroller re-enumeration"); + + 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(); + + 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); + } + } } void SdlInputHandler::cleanupDetachedGamepads() diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index c5f8f95b..3e4a71ec 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -19,6 +19,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i m_PendingMouseButtonsAllUpOnVideoRegionLeave(false), m_PointerRegionLockActive(false), m_PointerRegionLockToggledByUser(false), + m_LastJoystickCount(-1), m_FakeCaptureActive(false), m_CaptureSystemKeysMode(prefs.captureSysKeysMode), m_MouseCursorCapturedVisibilityState(SDL_DISABLE), diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index b0890b31..815d95f4 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -234,6 +234,7 @@ private: bool m_PointerRegionLockToggledByUser; int m_GamepadMask; + int m_LastJoystickCount; GamepadState m_GamepadState[MAX_GAMEPADS]; QSet m_KeysDown; bool m_FakeCaptureActive; From e94f0c59908db5ee6562f3b0709a76a6b656c421 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 07:40:45 -0700 Subject: [PATCH 17/21] Throttle hotplug re-enumeration retries and reduce log noise --- app/streaming/input/gamepad.cpp | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index deb8b4e1..b1391755 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -25,6 +25,7 @@ static bool isKeyboardMouseInputAllowed() #define MOUSE_EMULATION_POLLING_INTERVAL 50 #define HOTPLUG_REENUMERATION_INTERVAL_MS 2000 +#define HOTPLUG_REENUMERATION_MAX_INTERVAL_MS 8000 // Determines how fast the mouse will move each interval #define MOUSE_EMULATION_MOTION_MULTIPLIER 4 @@ -102,6 +103,8 @@ SdlInputHandler::ensureStateForGamepad(SDL_JoystickID 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; SDL_JoystickUpdate(); SDL_GameControllerUpdate(); @@ -138,6 +141,11 @@ void SdlInputHandler::pollForMissingGamepads() m_LastJoystickCount = joystickCount; } + if (joystickCount > 0) { + s_ReenumerationAttempts = 0; + s_ReenumerationIntervalMs = HOTPLUG_REENUMERATION_INTERVAL_MS; + } + for (int i = 0; i < MAX_GAMEPADS; i++) { GamepadState* state = &m_GamepadState[i]; if (state->controller == nullptr) { @@ -156,7 +164,7 @@ void SdlInputHandler::pollForMissingGamepads() continue; } - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Polling detected disconnected gamepad instance %d in slot %d; cleaning up", state->jsId, i); @@ -171,11 +179,14 @@ void SdlInputHandler::pollForMissingGamepads() if (joystickCount == 0) { uint32_t now = SDL_GetTicks(); - if (SDL_TICKS_PASSED(now, s_LastForcedReenumerationTick + HOTPLUG_REENUMERATION_INTERVAL_MS)) { + if (SDL_TICKS_PASSED(now, s_LastForcedReenumerationTick + s_ReenumerationIntervalMs)) { s_LastForcedReenumerationTick = now; + s_ReenumerationAttempts++; - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "No joysticks visible; forcing SDL joystick/gamecontroller re-enumeration"); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "No joysticks visible; forcing SDL joystick/gamecontroller re-enumeration (attempt %u, interval %u ms)", + s_ReenumerationAttempts, + s_ReenumerationIntervalMs); SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); SDL_QuitSubSystem(SDL_INIT_JOYSTICK); @@ -210,6 +221,15 @@ void SdlInputHandler::pollForMissingGamepads() } recoverUntrackedGamepads(joystickCount); + + if (joystickCount > 0) { + s_ReenumerationAttempts = 0; + s_ReenumerationIntervalMs = HOTPLUG_REENUMERATION_INTERVAL_MS; + } + else { + s_ReenumerationIntervalMs = qMin(s_ReenumerationIntervalMs * 2, + static_cast(HOTPLUG_REENUMERATION_MAX_INTERVAL_MS)); + } } } } @@ -777,7 +797,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve // before we've processed the add event. for (int i = 0; i < MAX_GAMEPADS; i++) { if (m_GamepadState[i].controller != nullptr && m_GamepadState[i].jsId == jsId) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Received duplicate add event for joystick instance ID: %d", jsId); SDL_GameControllerClose(controller); From d174341b6d2987b9ef92a35a428c952be7cdcba2 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 07:57:41 -0700 Subject: [PATCH 18/21] Centralize SDL input subsystem ownership for hotplug recovery --- app/app.pro | 2 + app/gui/sdlgamepadkeynavigation.cpp | 44 +++-- app/streaming/input/gamepad.cpp | 100 ++++++----- app/streaming/input/input.cpp | 69 +++----- app/streaming/input/input.h | 1 + app/streaming/input/sdlinputsubsystems.cpp | 191 +++++++++++++++++++++ app/streaming/input/sdlinputsubsystems.h | 25 +++ 7 files changed, 315 insertions(+), 117 deletions(-) create mode 100644 app/streaming/input/sdlinputsubsystems.cpp create mode 100644 app/streaming/input/sdlinputsubsystems.h 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/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index b1391755..810659fa 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -2,7 +2,7 @@ #include #include "SDL_compat.h" -#include "settings/mappingmanager.h" +#include "streaming/input/sdlinputsubsystems.h" #include @@ -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,54 +187,44 @@ 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 (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; + } - 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; - } + recoverUntrackedGamepads(joystickCount); - 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(); - - 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) { - s_ReenumerationAttempts = 0; - s_ReenumerationIntervalMs = HOTPLUG_REENUMERATION_INTERVAL_MS; + 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, @@ -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); diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 3e4a71ec..109f7b9b 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), @@ -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"); diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 815d95f4..6c177722 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -235,6 +235,7 @@ private: 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/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); + +} From 87f4e90ca25ba3880a4d1278a1fd615a3575d7c4 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 09:11:40 -0700 Subject: [PATCH 19/21] Split keyboard and mouse policy handling --- app/streaming/input/abstouch.cpp | 2 +- app/streaming/input/gamepad.cpp | 34 +++++++++--------- app/streaming/input/keyboard.cpp | 2 +- app/streaming/input/mouse.cpp | 2 +- app/streaming/input/reltouch.cpp | 2 +- app/streaming/session.cpp | 61 +++++++++++++++++++++----------- app/streaming/session.h | 15 ++++++-- 7 files changed, 75 insertions(+), 43 deletions(-) diff --git a/app/streaming/input/abstouch.cpp b/app/streaming/input/abstouch.cpp index 9e8b51a9..6c8529a2 100644 --- a/app/streaming/input/abstouch.cpp +++ b/app/streaming/input/abstouch.cpp @@ -11,7 +11,7 @@ static bool isKeyboardMouseInputAllowed() { auto session = Session::get(); - return session == nullptr || session->isKeyboardMouseInputAllowed(); + return session == nullptr || session->isMouseInputAllowed(); } // How long the fingers must be stationary to start a right click diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 810659fa..fdc896ab 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -12,10 +12,10 @@ static bool isGamepadInputAllowed() return session == nullptr || session->isGamepadInputAllowed(); } -static bool isKeyboardMouseInputAllowed() +static bool isMouseInputAllowed() { auto session = Session::get(); - return session == nullptr || session->isKeyboardMouseInputAllowed(); + return session == nullptr || session->isMouseInputAllowed(); } // How long the Start button must be pressed to toggle mouse emulation @@ -387,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) && isKeyboardMouseInputAllowed()) { + if ((deltaX != 0 || deltaY != 0) && isMouseInputAllowed()) { LiSendMouseMoveEvent((short)deltaX, (short)deltaY); } @@ -497,47 +497,47 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve } else if (state->mouseEmulationTimer != 0) { if (event->button == SDL_CONTROLLER_BUTTON_A) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); } } else if (event->button == SDL_CONTROLLER_BUTTON_B) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); } } else if (event->button == SDL_CONTROLLER_BUTTON_X) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE); } } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1); } } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2); } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendScrollEvent(1); } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendScrollEvent(-1); } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendHScrollEvent(1); } } else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendHScrollEvent(-1); } } @@ -570,27 +570,27 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve } else if (state->mouseEmulationTimer != 0) { if (event->button == SDL_CONTROLLER_BUTTON_A) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); } } else if (event->button == SDL_CONTROLLER_BUTTON_B) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); } } else if (event->button == SDL_CONTROLLER_BUTTON_X) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); } } else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1); } } else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) { - if (isKeyboardMouseInputAllowed()) { + if (isMouseInputAllowed()) { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2); } } diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index 7d5a03c4..ed12a6f2 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -16,7 +16,7 @@ static bool isKeyboardMouseInputAllowed() { auto session = Session::get(); - return session == nullptr || session->isKeyboardMouseInputAllowed(); + return session == nullptr || session->isKeyboardInputAllowed(); } void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) diff --git a/app/streaming/input/mouse.cpp b/app/streaming/input/mouse.cpp index c86eae54..d452ed07 100644 --- a/app/streaming/input/mouse.cpp +++ b/app/streaming/input/mouse.cpp @@ -8,7 +8,7 @@ static bool isKeyboardMouseInputAllowed() { auto session = Session::get(); - return session == nullptr || session->isKeyboardMouseInputAllowed(); + return session == nullptr || session->isMouseInputAllowed(); } void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) diff --git a/app/streaming/input/reltouch.cpp b/app/streaming/input/reltouch.cpp index 1649f1c1..6269537f 100644 --- a/app/streaming/input/reltouch.cpp +++ b/app/streaming/input/reltouch.cpp @@ -9,7 +9,7 @@ static bool isKeyboardMouseInputAllowed() { auto session = Session::get(); - return session == nullptr || session->isKeyboardMouseInputAllowed(); + return session == nullptr || session->isMouseInputAllowed(); } // How long the mouse button will be pressed for a tap to click gesture diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 7d27e095..c4d1d455 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -557,7 +557,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_ManualAudioMuted(false), m_AudioVolumeScalar(1.0f), m_AllowGamepadInput(true), - m_AllowKeyboardMouseInput(false), + m_AllowKeyboardInput(false), + m_AllowMouseInput(false), m_ControlPanelVisible(true), m_ConnectionStatus(CONN_STATUS_OKAY), m_StatusOverlayGeneration(0), @@ -627,9 +628,13 @@ void Session::notifyAudioVolumeState() void Session::setKeyboardMouseInputAllowed(bool allowed) { - bool previous = m_AllowKeyboardMouseInput.exchange(allowed, std::memory_order_relaxed); - if (previous && !allowed && m_InputHandler != nullptr) { + 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(); } @@ -656,11 +661,12 @@ void Session::toggleControlPanelVisibility() void Session::notifyInputPermissionState() { - char buffer[96]; + char buffer[160]; SDL_snprintf(buffer, sizeof(buffer), - "Keyboard/Mouse: %s\nGamepad: %s", - isKeyboardMouseInputAllowed() ? "ON" : "OFF", + "Input policy: host controlled\nKeyboard: %s\nMouse: %s\nGamepad: %s", + isKeyboardInputAllowed() ? "ON" : "OFF", + isMouseInputAllowed() ? "ON" : "OFF", isGamepadInputAllowed() ? "ON" : "OFF"); showTemporaryStatusOverlay(buffer); @@ -668,10 +674,11 @@ void Session::notifyInputPermissionState() void Session::sendInputPermissionStateToHost(uint8_t reason) { - const bool allowKeyboardMouse = isKeyboardMouseInputAllowed(); + const bool allowKeyboard = isKeyboardInputAllowed(); + const bool allowMouse = isMouseInputAllowed(); const bool allowGamepad = isGamepadInputAllowed(); - const int err = LiSendSessionInputPolicy(allowKeyboardMouse, - allowKeyboardMouse, + const int err = LiSendSessionInputPolicy(allowKeyboard, + allowMouse, allowGamepad, reason); @@ -719,11 +726,14 @@ void Session::refreshControlPanelOverlay() SDL_snprintf(panelText, sizeof(panelText), "Stream Controls\n" - "KB/M (Ctrl+Alt+Shift+K): %s\n" - "Pad (Ctrl+Alt+Shift+G): %s\n" + "Keyboard: %s\n" + "Mouse: %s\n" + "Pad: %s\n" "Vol (U/J, mute N): %s\n" - "UI (Ctrl+Alt+Shift+P / Select+L1+R1+B): ON", - isKeyboardMouseInputAllowed() ? "ON" : "OFF", + "UI (Ctrl+Alt+Shift+P / Select+L1+R1+B): ON\n" + "Input policy: host controlled", + isKeyboardInputAllowed() ? "ON" : "OFF", + isMouseInputAllowed() ? "ON" : "OFF", isGamepadInputAllowed() ? "ON" : "OFF", volumeState); @@ -774,10 +784,13 @@ void Session::showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs) void Session::applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool allowGamepad, uint8_t reason) { - const bool allowKeyboardMouse = allowKeyboard && allowMouse; - const bool previousKeyboardMouse = m_AllowKeyboardMouseInput.exchange(allowKeyboardMouse, std::memory_order_relaxed); - if (previousKeyboardMouse && !allowKeyboardMouse && m_InputHandler != nullptr) { + 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(); } @@ -786,11 +799,18 @@ void Session::applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool all m_InputHandler->raiseAllGamepadInputs(); } - if (reason != LI_SESSION_INPUT_POLICY_REASON_STREAM_START) { - notifyInputPermissionState(); + 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 { - refreshControlPanelOverlay(); + notifyInputPermissionState(); } } @@ -1975,7 +1995,8 @@ void Session::start() s_ActiveSession = this; m_AllowGamepadInput.store(true, std::memory_order_relaxed); - m_AllowKeyboardMouseInput.store(false, 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); diff --git a/app/streaming/session.h b/app/streaming/session.h index 13eca494..856e4aac 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -131,9 +131,19 @@ public: 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 m_AllowKeyboardMouseInput.load(std::memory_order_relaxed); + return isKeyboardInputAllowed() && isMouseInputAllowed(); } void setGamepadInputAllowed(bool allowed); @@ -298,7 +308,8 @@ private: std::atomic m_ManualAudioMuted; std::atomic m_AudioVolumeScalar; std::atomic m_AllowGamepadInput; - std::atomic m_AllowKeyboardMouseInput; + std::atomic m_AllowKeyboardInput; + std::atomic m_AllowMouseInput; std::atomic m_ControlPanelVisible; std::atomic m_ConnectionStatus; std::atomic m_StatusOverlayGeneration; From 25310655c58d3f570333877361250eb3a96957d7 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 09:27:36 -0700 Subject: [PATCH 20/21] Add separate keyboard and mouse input shortcuts --- app/streaming/input/gamepad.cpp | 17 +++++++++++-- app/streaming/input/input.cpp | 13 +++++++--- app/streaming/input/input.h | 3 ++- app/streaming/input/keyboard.cpp | 14 ++++++++--- app/streaming/session.cpp | 42 ++++++++++++++++++++++++++++---- app/streaming/session.h | 4 +++ 6 files changed, 78 insertions(+), 15 deletions(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index fdc896ab..6d5601a4 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -648,9 +648,22 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | Y_FLAG)) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected keyboard/mouse input toggle gamepad combo"); + "Detected keyboard input toggle gamepad combo"); - Session::get()->toggleKeyboardMouseInputAllowed(); + 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, diff --git a/app/streaming/input/input.cpp b/app/streaming/input/input.cpp index 109f7b9b..39a230a5 100644 --- a/app/streaming/input/input.cpp +++ b/app/streaming/input/input.cpp @@ -130,10 +130,15 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V; m_SpecialKeyCombos[KeyComboPasteText].enabled = true; - m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].keyCombo = KeyComboToggleKeyboardMouseInput; - m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].keyCode = SDLK_k; - m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].scanCode = SDL_SCANCODE_K; - m_SpecialKeyCombos[KeyComboToggleKeyboardMouseInput].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; diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 6c177722..46e3ae79 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -174,7 +174,8 @@ private: KeyComboToggleCursorHide, KeyComboToggleMinimize, KeyComboPasteText, - KeyComboToggleKeyboardMouseInput, + KeyComboToggleKeyboardInput, + KeyComboToggleMouseInput, KeyComboToggleGamepadInput, KeyComboVolumeUp, KeyComboVolumeDown, diff --git a/app/streaming/input/keyboard.cpp b/app/streaming/input/keyboard.cpp index ed12a6f2..ede74d51 100644 --- a/app/streaming/input/keyboard.cpp +++ b/app/streaming/input/keyboard.cpp @@ -142,11 +142,19 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo) break; } - case KeyComboToggleKeyboardMouseInput: + case KeyComboToggleKeyboardInput: SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Detected keyboard/mouse input toggle combo"); + "Detected keyboard input toggle combo"); if (auto session = Session::get(); session != nullptr) { - session->toggleKeyboardMouseInputAllowed(); + 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; diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index c4d1d455..668e9108 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -589,6 +589,28 @@ void Session::setGamepadInputAllowed(bool allowed) 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); @@ -647,6 +669,16 @@ void Session::toggleGamepadInputAllowed() setGamepadInputAllowed(!isGamepadInputAllowed()); } +void Session::toggleKeyboardInputAllowed() +{ + setKeyboardInputAllowed(!isKeyboardInputAllowed()); +} + +void Session::toggleMouseInputAllowed() +{ + setMouseInputAllowed(!isMouseInputAllowed()); +} + void Session::toggleKeyboardMouseInputAllowed() { setKeyboardMouseInputAllowed(!isKeyboardMouseInputAllowed()); @@ -726,11 +758,11 @@ void Session::refreshControlPanelOverlay() SDL_snprintf(panelText, sizeof(panelText), "Stream Controls\n" - "Keyboard: %s\n" - "Mouse: %s\n" - "Pad: %s\n" - "Vol (U/J, mute N): %s\n" - "UI (Ctrl+Alt+Shift+P / Select+L1+R1+B): ON\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", diff --git a/app/streaming/session.h b/app/streaming/session.h index 856e4aac..6289bf62 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -147,8 +147,12 @@ public: } 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(); From 20c327331437202059846753e96b751d3f390c6b Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 09:28:57 -0700 Subject: [PATCH 21/21] Restore control panel after renderer resets --- app/streaming/session.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 668e9108..01aa4800 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -2545,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;