diff --git a/app/streaming/input.cpp b/app/streaming/input.cpp index bef4dfc3..cbc2032a 100644 --- a/app/streaming/input.cpp +++ b/app/streaming/input.cpp @@ -81,6 +81,13 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s SDL_GetError()); } + 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()); + } + // 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. @@ -99,6 +106,9 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s SdlInputHandler::~SdlInputHandler() { for (int i = 0; i < MAX_GAMEPADS; i++) { + if (m_GamepadState[i].haptic != nullptr) { + SDL_HapticClose(m_GamepadState[i].haptic); + } if (m_GamepadState[i].controller != nullptr) { SDL_GameControllerClose(m_GamepadState[i].controller); } @@ -109,6 +119,9 @@ SdlInputHandler::~SdlInputHandler() SDL_RemoveTimer(m_RightButtonReleaseTimer); SDL_RemoveTimer(m_DragTimer); + SDL_QuitSubSystem(SDL_INIT_HAPTIC); + SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC)); + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); @@ -706,6 +719,12 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve state->controller = controller; state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller)); + state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller)); + state->hapticEffectId = -1; + if (state->haptic != nullptr && (SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) { + SDL_HapticClose(state->haptic); + state->haptic = nullptr; + } SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)), guidStr, sizeof(guidStr)); @@ -740,6 +759,9 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve state = findStateForGamepad(event->which); if (state != NULL) { SDL_GameControllerClose(state->controller); + if (state->haptic != nullptr) { + SDL_HapticClose(state->haptic); + } // Remove this from the gamepad mask in MC-mode if (m_MultiController) { @@ -793,6 +815,31 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event) } } +void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) +{ + SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic; + SDL_HapticEffect effect; + + if (haptic == nullptr) { + return; + } + + SDL_memset(&effect, 0, sizeof(effect)); + effect.type = SDL_HAPTIC_LEFTRIGHT; + effect.leftright.large_magnitude = lowFreqMotor; + effect.leftright.small_magnitude = highFreqMotor; + effect.leftright.length = 10000; // Choose 10 second max duration - can be cancelled sooner. + + if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { + SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId); + } + + m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect); + if (m_GamepadState[controllerNumber].hapticEffectId >= 0) { + SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1); + } +} + void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) { int fingerIndex = -1; diff --git a/app/streaming/input.h b/app/streaming/input.h index f007d67b..c29a8636 100644 --- a/app/streaming/input.h +++ b/app/streaming/input.h @@ -8,6 +8,8 @@ struct GamepadState { SDL_GameController* controller; SDL_JoystickID jsId; + SDL_Haptic* haptic; + int hapticEffectId; short index; short buttons; @@ -43,6 +45,8 @@ public: void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); + void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + void handleTouchFingerEvent(SDL_TouchFingerEvent* event); int getAttachedGamepadMask(); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 6ef8a1b6..8cca5d7b 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -38,7 +38,8 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { Session::clConnectionTerminated, nullptr, nullptr, - Session::clLogMessage + Session::clLogMessage, + Session::clRumble }; AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = { @@ -102,6 +103,11 @@ void Session::clLogMessage(const char* format, ...) va_end(ap); } +void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) +{ + s_ActiveSession->m_InputHandler->rumble(controllerNumber, lowFreqMotor, highFreqMotor); +} + #define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing) bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, @@ -295,7 +301,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_DisplayOriginX(0), m_DisplayOriginY(0), m_PendingWindowedTransition(false), - m_UnexpectedTermination(true), // Failure prior to streaming is unexpected + m_UnexpectedTermination(true), // Failure prior to streaming is unexpected + m_InputHandler(nullptr), m_OpusDecoder(nullptr), m_AudioRenderer(nullptr), m_AudioSampleCount(0) @@ -308,6 +315,8 @@ Session::~Session() // and the object is deallocated. s_ActiveSessionSemaphore.acquire(); s_ActiveSessionSemaphore.release(); + + delete m_InputHandler; } void Session::initialize() @@ -809,9 +818,9 @@ void Session::exec(int displayOriginX, int displayOriginY) // Initialize the gamepad code with our preferences StreamingPreferences prefs; - SdlInputHandler inputHandler(prefs, m_Computer, - m_StreamConfig.width, - m_StreamConfig.height); + m_InputHandler = new SdlInputHandler(prefs, m_Computer, + m_StreamConfig.width, + m_StreamConfig.height); // The UI should have ensured the old game was already quit // if we decide to stream a different game. @@ -842,7 +851,7 @@ void Session::exec(int displayOriginX, int displayOriginY) http.launchApp(m_App.id, &m_StreamConfig, enableGameOptimizations, prefs.playAudioOnHost, - inputHandler.getAttachedGamepadMask()); + m_InputHandler->getAttachedGamepadMask()); } } catch (const GfeHttpResponseException& e) { emit displayLaunchError(e.toQString()); @@ -1070,7 +1079,7 @@ void Session::exec(int displayOriginX, int displayOriginY) // Raise all keys that are currently pressed. If we don't do this, certain keys // used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down. - inputHandler.raiseAllKeys(); + m_InputHandler->raiseAllKeys(); } // We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event. @@ -1160,31 +1169,31 @@ void Session::exec(int displayOriginX, int displayOriginY) case SDL_KEYUP: case SDL_KEYDOWN: - inputHandler.handleKeyEvent(&event.key); + m_InputHandler->handleKeyEvent(&event.key); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: - inputHandler.handleMouseButtonEvent(&event.button); + m_InputHandler->handleMouseButtonEvent(&event.button); break; case SDL_MOUSEMOTION: - inputHandler.handleMouseMotionEvent(&event.motion); + m_InputHandler->handleMouseMotionEvent(&event.motion); break; case SDL_MOUSEWHEEL: - inputHandler.handleMouseWheelEvent(&event.wheel); + m_InputHandler->handleMouseWheelEvent(&event.wheel); break; case SDL_CONTROLLERAXISMOTION: - inputHandler.handleControllerAxisEvent(&event.caxis); + m_InputHandler->handleControllerAxisEvent(&event.caxis); break; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: - inputHandler.handleControllerButtonEvent(&event.cbutton); + m_InputHandler->handleControllerButtonEvent(&event.cbutton); break; case SDL_CONTROLLERDEVICEADDED: case SDL_CONTROLLERDEVICEREMOVED: - inputHandler.handleControllerDeviceEvent(&event.cdevice); + m_InputHandler->handleControllerDeviceEvent(&event.cdevice); break; case SDL_JOYDEVICEADDED: - inputHandler.handleJoystickArrivalEvent(&event.jdevice); + m_InputHandler->handleJoystickArrivalEvent(&event.jdevice); break; // SDL2 sends touch events from trackpads by default on @@ -1194,7 +1203,7 @@ void Session::exec(int displayOriginX, int displayOriginY) case SDL_FINGERDOWN: case SDL_FINGERMOTION: case SDL_FINGERUP: - inputHandler.handleTouchFingerEvent(&event.tfinger); + m_InputHandler->handleTouchFingerEvent(&event.tfinger); break; #endif } @@ -1208,7 +1217,7 @@ DispatchDeferredCleanup: SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0"); // Raise any keys that are still down - inputHandler.raiseAllKeys(); + m_InputHandler->raiseAllKeys(); // Destroy the decoder, since this must be done on the main thread SDL_AtomicLock(&m_DecoderLock); diff --git a/app/streaming/session.h b/app/streaming/session.h index 8d825643..0a7fb52d 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -97,6 +97,9 @@ private: static void clLogMessage(const char* format, ...); + static + void clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static int arInit(int audioConfiguration, const POPUS_MULTISTREAM_CONFIGURATION opusConfig, @@ -132,6 +135,7 @@ private: int m_DisplayOriginY; bool m_PendingWindowedTransition; bool m_UnexpectedTermination; + SdlInputHandler* m_InputHandler; int m_ActiveVideoFormat; int m_ActiveVideoWidth;