Add optional controller rumble on button press

New "Vibrate controller on button press" setting under Controls. When enabled,
pressing a gamepad button rumbles that pad for as long as it's held - something
the PSP hardware never had, but which some people want from a modern pad.

Implemented per platform behind System_ControllerRumbleStart/Stop: SDL uses
SDL_RumbleGamepad, Windows feeds it into XinputDevice's existing vibration path
so it doesn't fight with the vibration games request through sceCtrl, and
Android goes through InputDevice.getVibrator() in Java. Stubbed elsewhere.

The D-pad is excluded, since it gets tapped constantly while navigating.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
This commit is contained in:
HalexxanderandClaude Opus 5 authored and Henrik Rydgård committed 2026-09-05 12:08:09 -06:00
1 parent 6d4bc5f261
commit e5bd8e8e58
20 files changed
+173

No files matched your search

+3
View File
@@ -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.
+1
View File
@@ -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),
+1
View File
@@ -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;
+34
View File
@@ -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_;
+4
View File
@@ -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
+21
View File
@@ -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);
}
}
+1
View File
@@ -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);
+12
View File
@@ -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;
+1
View File
@@ -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.
+6
View File
@@ -669,6 +669,12 @@ std::vector<std::string> System_GetCameraDeviceList() {
void System_Vibrate(int length_ms) {
}
void System_ControllerRumbleStart(int deviceIndex) {
}
void System_ControllerRumbleStop(int deviceIndex) {
}
void System_AskForPermission(SystemPermission permission) {
}
+21
View File
@@ -2,6 +2,7 @@
#include <climits>
#include <algorithm>
#include <atomic>
#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<bool> 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;
}
}
+6
View File
@@ -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;
+9
View File
@@ -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";
+14
View File
@@ -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 "<start>,<deviceIndex>".
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;
@@ -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,<deviceIndex>" = start, "0,<deviceIndex>" = 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;
+1
View File
@@ -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
+2
View File
@@ -128,6 +128,8 @@ void System_RunOnMainThread(std::function<void()>) {}
std::vector<std::string> System_GetCameraDeviceList() { return std::vector<std::string>(); }
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) {}
+6
View File
@@ -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;
+2
View File
@@ -1985,6 +1985,8 @@ void System_Notify(SystemNotification notification) {
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string &param1, const std::string &param2, int64_t param3, int64_t param4) { return false; }
void System_PostUIMessage(UIMessage message, std::string_view param) {}
void System_RunOnMainThread(std::function<void()>) {}
void System_ControllerRumbleStart(int deviceIndex) {}
void System_ControllerRumbleStop(int deviceIndex) {}
void NativeFrame(GraphicsContext *graphicsContext) {}
void NativeResized() {}
void System_Toast(std::string_view str) {}
+2
View File
@@ -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<void(bool, const std::string &)> 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".