From df283d80c6e9115f70361045f990eb5a2f592159 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 2 Jul 2023 16:44:41 -0500 Subject: [PATCH] Implement controller LED and battery protocol extensions --- app/streaming/input/gamepad.cpp | 82 +++++++++++++++++++++++++++ app/streaming/input/input.h | 8 +++ app/streaming/session.cpp | 26 +++++++++ app/streaming/session.h | 3 + moonlight-common-c/moonlight-common-c | 2 +- 5 files changed, 120 insertions(+), 1 deletion(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index 62b0c4cf..2463c800 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -64,6 +64,47 @@ void SdlInputHandler::sendGamepadState(GamepadState* state) state->rsY); } +void SdlInputHandler::sendGamepadBatteryState(GamepadState* state, SDL_JoystickPowerLevel level) +{ + uint8_t batteryPercentage; + uint8_t batteryState; + + // SDL's battery reporting capabilities are quite limited. Notably, we cannot + // tell the battery level while charging (or even if a battery is present). + // We also cannot tell the percentage of charge exactly in any case. + switch (level) + { + case SDL_JOYSTICK_POWER_UNKNOWN: + batteryState = LI_BATTERY_STATE_UNKNOWN; + batteryPercentage = LI_BATTERY_PERCENTAGE_UNKNOWN; + break; + case SDL_JOYSTICK_POWER_WIRED: + batteryState = LI_BATTERY_STATE_CHARGING; + batteryPercentage = LI_BATTERY_PERCENTAGE_UNKNOWN; + break; + case SDL_JOYSTICK_POWER_EMPTY: + batteryState = LI_BATTERY_STATE_DISCHARGING; + batteryPercentage = 5; + break; + case SDL_JOYSTICK_POWER_LOW: + batteryState = LI_BATTERY_STATE_DISCHARGING; + batteryPercentage = 20; + break; + case SDL_JOYSTICK_POWER_MEDIUM: + batteryState = LI_BATTERY_STATE_DISCHARGING; + batteryPercentage = 50; + break; + case SDL_JOYSTICK_POWER_FULL: + batteryState = LI_BATTERY_STATE_DISCHARGING; + batteryPercentage = 90; + break; + default: + return; + } + + LiSendControllerBatteryEvent(state->index, batteryState, batteryPercentage); +} + Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param) { auto gamepad = reinterpret_cast(param); @@ -364,6 +405,20 @@ void SdlInputHandler::handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* #endif +#if SDL_VERSION_ATLEAST(2, 24, 0) + +void SdlInputHandler::handleJoystickBatteryEvent(SDL_JoyBatteryEvent* event) +{ + GamepadState* state = findStateForGamepad(event->which); + if (state == NULL) { + return; + } + + sendGamepadBatteryState(state, event->level); +} + +#endif + void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) { GamepadState* state; @@ -489,6 +544,8 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve SDL_assert(m_GamepadMask == 0x1); } + SDL_JoystickPowerLevel powerLevel = SDL_JoystickCurrentPowerLevel(SDL_GameControllerGetJoystick(state->controller)); + #if SDL_VERSION_ATLEAST(2, 0, 14) // On SDL 2.0.14 and later, we can provide enhanced controller information to the host PC // for it to use as a hint for the type of controller to emulate. @@ -520,6 +577,12 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve if (SDL_GameControllerHasSensor(state->controller, SDL_SENSOR_GYRO)) { capabilities |= LI_CCAP_GYRO; } + if (powerLevel != SDL_JOYSTICK_POWER_UNKNOWN || SDL_VERSION_ATLEAST(2, 24, 0)) { + capabilities |= LI_CCAP_BATTERY_STATE; + } + if (SDL_GameControllerHasLED(state->controller)) { + capabilities |= LI_CCAP_RGB_LED; + } uint8_t type; switch (SDL_GameControllerGetType(state->controller)) { @@ -549,6 +612,11 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve // Send an empty event to tell the PC we've arrived sendGamepadState(state); #endif + + // Send a power level if it's known at this time + if (powerLevel != SDL_JOYSTICK_POWER_UNKNOWN) { + sendGamepadBatteryState(state, powerLevel); + } } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { state = findStateForGamepad(event->which); @@ -716,6 +784,20 @@ void SdlInputHandler::setMotionEventState(uint16_t controllerNumber, uint8_t mot #endif } +void SdlInputHandler::setControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) +{ + // Make sure the controller number is within our supported count + if (controllerNumber >= MAX_GAMEPADS) { + return; + } + +#if SDL_VERSION_ATLEAST(2, 0, 14) + if (m_GamepadState[controllerNumber].controller != nullptr) { + SDL_GameControllerSetLED(m_GamepadState[controllerNumber].controller, r, g, b); + } +#endif +} + QString SdlInputHandler::getUnmappedGamepads() { QString ret; diff --git a/app/streaming/input/input.h b/app/streaming/input/input.h index 848f1421..c146a8f2 100644 --- a/app/streaming/input/input.h +++ b/app/streaming/input/input.h @@ -72,6 +72,10 @@ public: void handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* event); #endif +#if SDL_VERSION_ATLEAST(2, 24, 0) + void handleJoystickBatteryEvent(SDL_JoyBatteryEvent* event); +#endif + void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); void sendText(QString& string); @@ -82,6 +86,8 @@ public: void setMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + void setControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + void handleTouchFingerEvent(SDL_TouchFingerEvent* event); int getAttachedGamepadMask(); @@ -126,6 +132,8 @@ private: void sendGamepadState(GamepadState* state); + void sendGamepadBatteryState(GamepadState* state, SDL_JoystickPowerLevel level); + void handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event); void emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index d23a0835..2c06359a 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -39,6 +39,7 @@ #define SDL_CODE_GAMECONTROLLER_RUMBLE 101 #define SDL_CODE_GAMECONTROLLER_RUMBLE_TRIGGERS 102 #define SDL_CODE_GAMECONTROLLER_SET_MOTION_EVENT_STATE 103 +#define SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED 104 #include @@ -66,6 +67,7 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { Session::clSetHdrMode, Session::clRumbleTriggers, Session::clSetMotionEventState, + Session::clSetControllerLED, }; Session* Session::s_ActiveSession; @@ -243,6 +245,19 @@ void Session::clSetMotionEventState(uint16_t controllerNumber, uint8_t motionTyp SDL_PushEvent(&setMotionEventStateEvent); } +void Session::clSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) +{ + // We push an event for the main thread to handle in order to properly synchronize + // with the removal of game controllers that could result in our game controller + // going away during this callback. + SDL_Event setControllerLEDEvent = {}; + setControllerLEDEvent.type = SDL_USEREVENT; + setControllerLEDEvent.user.code = SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED; + setControllerLEDEvent.user.data1 = (void*)(uintptr_t)controllerNumber; + setControllerLEDEvent.user.data2 = (void*)(uintptr_t)(r << 16 | g << 8 | b); + SDL_PushEvent(&setControllerLEDEvent); +} + bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, SDL_Window* window, int videoFormat, int width, int height, int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder) @@ -1679,6 +1694,12 @@ void Session::execInternal() (uint8_t)((uintptr_t)event.user.data2 >> 16), (uint16_t)((uintptr_t)event.user.data2 & 0xFFFF)); break; + case SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED: + m_InputHandler->setControllerLED((uint16_t)(uintptr_t)event.user.data1, + (uint8_t)((uintptr_t)event.user.data2 >> 16), + (uint8_t)((uintptr_t)event.user.data2 >> 8), + (uint8_t)((uintptr_t)event.user.data2)); + break; default: SDL_assert(false); } @@ -1874,6 +1895,11 @@ void Session::execInternal() case SDL_CONTROLLERTOUCHPADMOTION: m_InputHandler->handleControllerTouchpadEvent(&event.ctouchpad); break; +#endif +#if SDL_VERSION_ATLEAST(2, 24, 0) + case SDL_JOYBATTERYUPDATED: + m_InputHandler->handleJoystickBatteryEvent(&event.jbattery); + break; #endif case SDL_CONTROLLERDEVICEADDED: case SDL_CONTROLLERDEVICEREMOVED: diff --git a/app/streaming/session.h b/app/streaming/session.h index e96ca761..26c5102c 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -130,6 +130,9 @@ private: static void clSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static + void clSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int arInit(int audioConfiguration, const POPUS_MULTISTREAM_CONFIGURATION opusConfig, diff --git a/moonlight-common-c/moonlight-common-c b/moonlight-common-c/moonlight-common-c index 28d63b11..c0792168 160000 --- a/moonlight-common-c/moonlight-common-c +++ b/moonlight-common-c/moonlight-common-c @@ -1 +1 @@ -Subproject commit 28d63b11ddb808662f0a7d90674a1376d99059c4 +Subproject commit c0792168f5a7ed48fc6feeb7fce01b83df405df2