mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
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:
1 parent
6d4bc5f261
commit
e5bd8e8e58
20 files changed
+173
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()>) {}
|
||||
void System_ControllerRumbleStart(int deviceIndex) {}
|
||||
void System_ControllerRumbleStop(int deviceIndex) {}
|
||||
void NativeFrame(GraphicsContext *graphicsContext) {}
|
||||
void NativeResized() {}
|
||||
void System_Toast(std::string_view str) {}
|
||||
|
||||
@@ -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".
|
||||
|
||||
Reference in new issue
Block a user