diff --git a/Common/System/System.h b/Common/System/System.h index d2ce08d675..4c37c5a691 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -52,6 +52,9 @@ enum class LaunchUrlType { }; void System_Vibrate(int length_ms); +// Rumbles a game controller for as long as a button is held. deviceIndex is 0-based. +void System_ControllerRumbleStart(int deviceIndex); +void System_ControllerRumbleStop(int deviceIndex); void System_LaunchUrl(LaunchUrlType urlType, std::string_view url); // It's sometimes a little unclear what should be a request, and what should be a separate function. diff --git a/Core/Config.cpp b/Core/Config.cpp index 2f7e0952f3..a3fb8b3a43 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -946,6 +946,7 @@ static const ConfigSetting touchControlSettings[] = { static const ConfigSetting controlSettings[] = { ConfigSetting("HapticFeedback", SETTING(g_Config, bHapticFeedback), false, CfgFlag::PER_GAME), + ConfigSetting("ControllerButtonRumble", SETTING(g_Config, bControllerButtonRumble), false, CfgFlag::PER_GAME), #if PPSSPP_PLATFORM(WINDOWS) ConfigSetting("IgnoreWindowsKey", SETTING(g_Config, bIgnoreWindowsKey), false, CfgFlag::PER_GAME), diff --git a/Core/Config.h b/Core/Config.h index cb4ebc95a6..6b2c0d2981 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -520,6 +520,7 @@ public: bool bShowTouchPause; bool bHapticFeedback; + bool bControllerButtonRumble; // We also use the XInput settings as analog settings on other platforms like Android. float fAnalogDeadzone; diff --git a/Core/ControlMapper.cpp b/Core/ControlMapper.cpp index b336568be7..5e5ea128a8 100644 --- a/Core/ControlMapper.cpp +++ b/Core/ControlMapper.cpp @@ -7,6 +7,7 @@ #include "Common/StringUtils.h" #include "Common/Log.h" +#include "Common/System/System.h" #include "Core/HLE/sceCtrl.h" #include "Core/KeyMap.h" #include "Core/ControlMapper.h" @@ -615,9 +616,42 @@ bool ControlMapper::Key(const KeyInput &key) { KeyMap::LockMappings(); bool retval = UpdatePSPState(mapping, now); KeyMap::UnlockMappings(); + + if (g_Config.bControllerButtonRumble) { + UpdateButtonRumble(key); + } + return retval; } +// Rumbles the pad for as long as a button is held. Note that we count the buttons held +// per pad - otherwise releasing one of two held buttons would cut the rumble short. +void ControlMapper::UpdateButtonRumble(const KeyInput &key) { + int padIndex; + if (key.deviceId >= DEVICE_ID_PAD_0 && key.deviceId <= DEVICE_ID_PAD_9) { + padIndex = (int)key.deviceId - DEVICE_ID_PAD_0; + } else if (key.deviceId >= DEVICE_ID_XINPUT_0 && key.deviceId <= DEVICE_ID_XINPUT_3) { + padIndex = (int)key.deviceId - DEVICE_ID_XINPUT_0; + } else { + return; + } + + // The D-pad gets tapped constantly when navigating, which just feels noisy. + if (key.keyCode == NKCODE_UNKNOWN || (key.keyCode >= NKCODE_DPAD_UP && key.keyCode <= NKCODE_DPAD_RIGHT)) { + return; + } + + if (key.flags & KeyInputFlags::DOWN) { + if (buttonsHeld_[padIndex]++ == 0) { + System_ControllerRumbleStart(padIndex); + } + } else if (key.flags & KeyInputFlags::UP) { + if (buttonsHeld_[padIndex] > 0 && --buttonsHeld_[padIndex] == 0) { + System_ControllerRumbleStop(padIndex); + } + } +} + void ControlMapper::ToggleSwapAxes() { // Note: The lock is already locked here. swapAxes_ = !swapAxes_; diff --git a/Core/ControlMapper.h b/Core/ControlMapper.h index d28f526b45..99053a8a67 100644 --- a/Core/ControlMapper.h +++ b/Core/ControlMapper.h @@ -72,6 +72,7 @@ public: private: void UpdateSwapAxes(); + void UpdateButtonRumble(const KeyInput &key); bool UpdatePSPState(const InputMapping &changedMapping, double now); float MapAxisValue(float value, int vkId, const InputMapping &mapping, const InputMapping &changedMapping, bool *oppositeTouched); void SwapMappingIfEnabled(uint32_t *vkey); @@ -116,6 +117,9 @@ private: bool swapAxes_ = false; + // Buttons held per pad, for the optional rumble-on-button-press. + int buttonsHeld_[10]{}; + int iInternalScreenRotationCached_ = 0; // Protects basically all the state. (There is no screenmanager mutex to piggyback on, despite diff --git a/SDL/SDLJoystick.cpp b/SDL/SDLJoystick.cpp index 7bd1c63783..5bd43009b3 100644 --- a/SDL/SDLJoystick.cpp +++ b/SDL/SDLJoystick.cpp @@ -265,3 +265,24 @@ int SDLJoystick::getDeviceIndex(int instanceId) { } return it->second; } + +void SDLJoystick::Rumble(int padIndex, bool start) { + // We currently force every pad to pad 0 (see ProcessInput), so in practice this + // rumbles the first connected controller. Indexing by pad keeps working if we ever + // stop doing that. + if (padIndex < 0 || padIndex >= (int)controllers.size()) { + return; + } + SDL_Gamepad *gamepad = controllers[padIndex]; + if (!SDL_GamepadConnected(gamepad)) { + return; + } + if (start) { + // Duration is in milliseconds - an hour stands in for "until we stop it". + if (!SDL_RumbleGamepad(gamepad, 0xFFFF, 0xFFFF, 3600000)) { + DEBUG_LOG(Log::System, "Rumble start failed: %s", SDL_GetError()); + } + } else { + SDL_RumbleGamepad(gamepad, 0, 0, 0); + } +} diff --git a/SDL/SDLJoystick.h b/SDL/SDLJoystick.h index 4fd715b482..3d68998ce0 100644 --- a/SDL/SDLJoystick.h +++ b/SDL/SDLJoystick.h @@ -14,6 +14,7 @@ public: void registerEventHandler(); void ProcessInput(const SDL_Event &event); + void Rumble(int padIndex, bool start); private: void setUpController(SDL_JoystickID deviceID); diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 57aa4a608a..8999d62c3b 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -646,6 +646,18 @@ void System_Vibrate(int length_ms) { // Ignore on PC } +void System_ControllerRumbleStart(int deviceIndex) { + if (joystick) { + joystick->Rumble(deviceIndex, true); + } +} + +void System_ControllerRumbleStop(int deviceIndex) { + if (joystick) { + joystick->Rumble(deviceIndex, false); + } +} + AudioBackend *System_CreateAudioBackend() { // Use legacy mechanisms. return nullptr; diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 5d158d31ce..dd2e83c862 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -870,6 +870,7 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_MOBILE) { controlsSettings->Add(new CheckBox(&g_Config.bHapticFeedback, co->T("HapticFeedback", "Haptic Feedback (vibration)"))); } + controlsSettings->Add(new CheckBox(&g_Config.bControllerButtonRumble, co->T("ControllerButtonRumble", "Vibrate controller on button press"))); // The pause button is now a regular on-screen button. diff --git a/UWP/PPSSPP_UWPMain.cpp b/UWP/PPSSPP_UWPMain.cpp index 7f585c1ea5..21e6beb482 100644 --- a/UWP/PPSSPP_UWPMain.cpp +++ b/UWP/PPSSPP_UWPMain.cpp @@ -669,6 +669,12 @@ std::vector System_GetCameraDeviceList() { void System_Vibrate(int length_ms) { } +void System_ControllerRumbleStart(int deviceIndex) { +} + +void System_ControllerRumbleStop(int deviceIndex) { +} + void System_AskForPermission(SystemPermission permission) { } diff --git a/Windows/XinputDevice.cpp b/Windows/XinputDevice.cpp index 64a6c5e62d..8189f3f86d 100644 --- a/Windows/XinputDevice.cpp +++ b/Windows/XinputDevice.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "Common/System/NativeApp.h" #include "Common/CommonWindows.h" @@ -123,6 +124,15 @@ static void UnloadXInputDLL() {} #define XUSER_MAX_COUNT 4 #endif +// Written from the input thread by XinputSetButtonRumble, read by ApplyVibration. +static std::atomic g_buttonRumble[XUSER_MAX_COUNT]; + +void XinputSetButtonRumble(int pad, bool on) { + if (pad >= 0 && pad < XUSER_MAX_COUNT) { + g_buttonRumble[pad] = on; + } +} + // Undocumented. Steam annoyingly grabs this button though.... #define XINPUT_GUIDE_BUTTON 0x400 @@ -307,6 +317,7 @@ void XinputDevice::ApplyVibration(int pad, XINPUT_VIBRATION &vibration) { vibration.wLeftMotorSpeed = 0; vibration.wRightMotorSpeed = 0; } + ApplyButtonRumble(pad, vibration); if (padData_[pad].prevVibration.wLeftMotorSpeed != vibration.wLeftMotorSpeed || padData_[pad].prevVibration.wRightMotorSpeed != vibration.wRightMotorSpeed) { PPSSPP_XInputSetState(pad, &vibration); @@ -315,9 +326,19 @@ void XinputDevice::ApplyVibration(int pad, XINPUT_VIBRATION &vibration) { prevVibrationTime_ = newVibrationTime_; } } else { + ApplyButtonRumble(pad, vibration); DWORD dwResult = PPSSPP_XInputSetState(pad, &vibration); if (dwResult != ERROR_SUCCESS) { padData_[pad].checkDelayUpdates = 30; } } } + +void XinputDevice::ApplyButtonRumble(int pad, XINPUT_VIBRATION &vibration) { + // Runs the motors flat out for as long as the button is held, overriding whatever + // the game asked for - it's an explicit user setting, so it wins. + if (g_buttonRumble[pad]) { + vibration.wLeftMotorSpeed = 0xFFFF; + vibration.wRightMotorSpeed = 0xFFFF; + } +} diff --git a/Windows/XinputDevice.h b/Windows/XinputDevice.h index 13c489451f..ff954ea5e2 100644 --- a/Windows/XinputDevice.h +++ b/Windows/XinputDevice.h @@ -4,6 +4,11 @@ #include "Core/HLE/sceCtrl.h" #include "Windows/InputDevice.h" +// Button rumble ("Vibrate controller on button press"), driven from +// System_ControllerRumbleStart/Stop. Mixed into whatever vibration the game itself +// asked for, so the two don't fight over the motors. +void XinputSetButtonRumble(int pad, bool on); + class XinputDevice final : public InputDevice { public: XinputDevice(); @@ -15,6 +20,7 @@ private: void ReleaseAllKeys(int pad); void ApplyButtons(int pad, const XINPUT_STATE &state); void ApplyVibration(int pad, XINPUT_VIBRATION &vibration); + void ApplyButtonRumble(int pad, XINPUT_VIBRATION &vibration); struct PadData { bool connected = false; diff --git a/Windows/main.cpp b/Windows/main.cpp index 1de6919fd8..b159c4a389 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -85,6 +85,7 @@ #include "Windows/W32Util/DialogManager.h" #include "Windows/W32Util/DarkMode.h" #include "Windows/W32Util/ShellUtil.h" +#include "Windows/XinputDevice.h" #include "Windows/Debugger/CtrlDisAsmView.h" #include "Windows/Debugger/CtrlMemView.h" @@ -166,6 +167,14 @@ void System_Vibrate(int length_ms) { // if it was used within the last few seconds. } +void System_ControllerRumbleStart(int deviceIndex) { + XinputSetButtonRumble(deviceIndex, true); +} + +void System_ControllerRumbleStop(int deviceIndex) { + XinputSetButtonRumble(deviceIndex, false); +} + static void AddDebugRestartArgs() { if (g_logManager.GetConsoleListener()->IsOpen()) { g_restartArgs += " -l"; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 6ec11565c2..75d6c0cf4d 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -280,6 +280,20 @@ void System_Vibrate(int length_ms) { PushCommand("vibrate", temp); } +// Handled in Java, so it works for both Bluetooth and USB pads through +// InputDevice.getVibrator(). Params are ",". +void System_ControllerRumbleStart(int deviceIndex) { + char temp[32]; + snprintf(temp, sizeof(temp), "1,%d", deviceIndex); + PushCommand("controllerRumble", temp); +} + +void System_ControllerRumbleStop(int deviceIndex) { + char temp[32]; + snprintf(temp, sizeof(temp), "0,%d", deviceIndex); + PushCommand("controllerRumble", temp); +} + void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) { switch (urlType) { case LaunchUrlType::BROWSER_URL: PushCommand("launchBrowser", url); break; diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index ba6dc18e3a..14508aee35 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -29,6 +29,7 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.VibrationEffect; import android.os.Environment; import android.os.PowerManager; import android.provider.MediaStore; @@ -1665,6 +1666,31 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList Log.e(TAG, "Can't vibrate, no surface view"); } return true; + + } else if (command.equals("controllerRumble")) { + // Controller rumble, requested by the core when "Vibrate controller on button + // press" is enabled. Params: "1," = start, "0," = stop. + // PPSSPP maps every connected pad to DEVICE_ID_PAD_0, so we rumble all of them. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + boolean start = params != null && params.startsWith("1"); + for (InputDeviceState state : inputPlayers) { + if (state == null || state.getDevice() == null) { + continue; + } + android.os.Vibrator vibrator = state.getDevice().getVibrator(); + if (vibrator == null || !vibrator.hasVibrator()) { + continue; + } + if (start) { + // Repeats from index 0, so it runs until we cancel it - that is, until + // the button comes back up. + vibrator.vibrate(VibrationEffect.createWaveform(new long[]{1000}, new int[]{255}, 0)); + } else { + vibrator.cancel(); + } + } + } + return true; } else if (command.equals("finish")) { Log.i(TAG, "Setting shuttingDown = true and calling Finish"); shuttingDown = true; diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 5b8dd9a2f0..2604d7b5ac 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -164,6 +164,7 @@ Gesture = Gesture Gesture mapping = Gesture mapping Glowing borders = Glowing borders HapticFeedback = Haptic feedback (vibration) +ControllerButtonRumble = Vibrate controller on button press Hide touch analog stick background circle = Hide touch analog stick background circle Icon = Icon Ignore gamepads when not focused = Ignore gamepads when not focused diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 30d38546cf..42ca30d31b 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -128,6 +128,8 @@ void System_RunOnMainThread(std::function) {} std::vector System_GetCameraDeviceList() { return std::vector(); } void System_AskForPermission(SystemPermission permission) {} PermissionStatus System_GetPermissionStatus(SystemPermission permission) { return PERMISSION_STATUS_GRANTED; } +void System_ControllerRumbleStart(int deviceIndex) {} +void System_ControllerRumbleStop(int deviceIndex) {} void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) buf[0] = '\0'; } void System_AudioClear() {} void System_AudioPushSamples(const s32 *audio, int numSamples, float volume) {} diff --git a/ios/main.mm b/ios/main.mm index ccbcee2576..fd9d82e1c1 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -659,6 +659,12 @@ void System_Vibrate(int mode) { } } +void System_ControllerRumbleStart(int deviceIndex) { +} + +void System_ControllerRumbleStop(int deviceIndex) { +} + AudioBackend *System_CreateAudioBackend() { // Use legacy mechanisms. return nullptr; diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index 9af1ce8169..141d2c1706 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -1985,6 +1985,8 @@ void System_Notify(SystemNotification notification) { bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) { return false; } void System_PostUIMessage(UIMessage message, std::string_view param) {} void System_RunOnMainThread(std::function) {} +void System_ControllerRumbleStart(int deviceIndex) {} +void System_ControllerRumbleStop(int deviceIndex) {} void NativeFrame(GraphicsContext *graphicsContext) {} void NativeResized() {} void System_Toast(std::string_view str) {} diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 9b9913cdb7..0ae883698e 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -154,6 +154,8 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {} void System_InputBoxGetString(const std::string &title, const std::string &defaultValue, std::function cb) { cb(false, ""); } void System_AskForPermission(SystemPermission permission) {} +void System_ControllerRumbleStart(int deviceIndex) {} +void System_ControllerRumbleStop(int deviceIndex) {} PermissionStatus System_GetPermissionStatus(SystemPermission permission) { return PERMISSION_STATUS_GRANTED; } // TODO: To avoid having to define these here, these should probably be turned into system "requests".