mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Send rumble to the controller, not the phone
Rumble always went to the phone's own motor: the vibrator lookup asked the system service and never considered the connected pad. On a handheld that is the wrong motor outright, and on a phone-plus-controller setup it buzzes the device sitting in a dock while the pad in hand stays still. Prefer the first connected gamepad or joystick reporting a working motor, falling back to the phone. The target is resolved per state change rather than cached for the pump's lifetime, so connecting or disconnecting a pad mid-session moves rumble with it; the motor that was last started is tracked separately so unplugging mid-rumble cannot leave one buzzing. Add a 'Vibrate the phone' toggle gating only that fallback, so playing on a pad need not mean the phone rumbles along with it. Touch haptics stay on the phone deliberately -- the finger is on the phone's screen. Closes #89
This commit is contained in:
Vendored
+1
-1
Submodule 3rdparty/discord-rpc/discord-rpc updated: fb04b1766b...3dc2c326cb
Vendored
+1
-1
Submodule 3rdparty/libsdl-org/SDL updated: 147a8ee32d...f87239e71e
@@ -72,7 +72,13 @@
|
|||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Savers", __VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Savers", __VA_ARGS__)
|
||||||
|
|
||||||
#define STACK_DEPTH 32
|
#define STACK_DEPTH 32
|
||||||
#define GL1_MAX_LISTS 16
|
/* Sixteen was not enough for two of the savers, and the failure was silent-ish: glGenLists
|
||||||
|
* returned 0, every subsequent glNewList/glCallList logged "bad list", and the geometry those
|
||||||
|
* lists held simply never drew. Lattice asks for 20 in one range (NUMOBJECTS), and Skyrocket
|
||||||
|
* accumulates 16 across separate calls -- flare 4, smoke 5, and seven singles in world.cpp.
|
||||||
|
* Names are 1-based, so the old ceiling allowed a longest run of 15 and neither could ever
|
||||||
|
* succeed. Sixty-four is a slot table of a pointer and two ints apiece: about a kilobyte. */
|
||||||
|
#define GL1_MAX_LISTS 64
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
float pos[3];
|
float pos[3];
|
||||||
|
|||||||
@@ -906,6 +906,9 @@ val EN: Map<String, String> = mapOf(
|
|||||||
"pad.players.help" to "The PS3 has seven controller ports and no multitap, so up to seven pads work with no setup. Connect them before launching — the order they first press a button in is the order they are assigned.",
|
"pad.players.help" to "The PS3 has seven controller ports and no multitap, so up to seven pads work with no setup. Connect them before launching — the order they first press a button in is the order they are assigned.",
|
||||||
"pad.rumble.description" to "Master switch for controller rumble and the device's built-in vibration. Turn off to silence all haptics.",
|
"pad.rumble.description" to "Master switch for controller rumble and the device's built-in vibration. Turn off to silence all haptics.",
|
||||||
"pad.rumble.label" to "Rumble / Vibration",
|
"pad.rumble.label" to "Rumble / Vibration",
|
||||||
|
"pad.rumblePhone.label" to "Vibrate the phone",
|
||||||
|
"pad.rumblePhone.description" to
|
||||||
|
"Use the phone's own motor when no controller has one. Turn this off to keep rumble on the controller only.",
|
||||||
"pad.hapticStrength.description" to "Scales all vibration — controller rumble and on-screen touch haptics alike. Below 100% tames a strong motor; above 100% boosts a weak one.",
|
"pad.hapticStrength.description" to "Scales all vibration — controller rumble and on-screen touch haptics alike. Below 100% tames a strong motor; above 100% boosts a weak one.",
|
||||||
"pad.hapticStrength.label" to "Vibration Strength",
|
"pad.hapticStrength.label" to "Vibration Strength",
|
||||||
"pad.scopeHint.global" to "○ Editing GLOBAL controls (all games).",
|
"pad.scopeHint.global" to "○ Editing GLOBAL controls (all games).",
|
||||||
|
|||||||
@@ -393,6 +393,17 @@ object ControllerMappings {
|
|||||||
com.armsx3.NativeApp.sRumbleEnabled = on
|
com.armsx3.NativeApp.sRumbleEnabled = on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether the PHONE's motor may be used. Rumble prefers a connected controller's motor and
|
||||||
|
// falls back to the phone; this gates only that fallback, so playing on a pad need not mean
|
||||||
|
// the phone buzzes too. Mirrored into NativeApp.sPhoneRumbleEnabled the same way KEY_RUMBLE
|
||||||
|
// is — live on change and at app start. Default on, so a phone-only player is unaffected.
|
||||||
|
private const val KEY_RUMBLE_PHONE = "pad.rumble.phone"
|
||||||
|
fun phoneRumbleEnabled(): Boolean = MainActivityRuntime.prefs.getBoolean(KEY_RUMBLE_PHONE, true)
|
||||||
|
fun setPhoneRumbleEnabled(on: Boolean) {
|
||||||
|
MainActivityRuntime.prefs.edit { putBoolean(KEY_RUMBLE_PHONE, on) }
|
||||||
|
com.armsx3.NativeApp.sPhoneRumbleEnabled = on
|
||||||
|
}
|
||||||
|
|
||||||
// Haptic strength: one multiplier scaling ALL vibration — controller rumble AND on-screen
|
// Haptic strength: one multiplier scaling ALL vibration — controller rumble AND on-screen
|
||||||
// touch ticks both funnel through NativeApp.rumbleOne. 0..200 % (100 = as the game/UI
|
// touch ticks both funnel through NativeApp.rumbleOne. 0..200 % (100 = as the game/UI
|
||||||
// authored it), so it tames a too-strong motor or boosts a weak one. Persisted and mirrored
|
// authored it), so it tames a too-strong motor or boosts a weak one. Persisted and mirrored
|
||||||
|
|||||||
@@ -2157,6 +2157,7 @@ open class MainActivityRuntime : ComponentActivity() {
|
|||||||
startAutosaveIntervalJob()
|
startAutosaveIntervalJob()
|
||||||
// Restore the saved rumble master toggle into the native gate (NativeApp.onPadRumble).
|
// Restore the saved rumble master toggle into the native gate (NativeApp.onPadRumble).
|
||||||
NativeApp.sRumbleEnabled = ControllerMappings.rumbleEnabled()
|
NativeApp.sRumbleEnabled = ControllerMappings.rumbleEnabled()
|
||||||
|
NativeApp.sPhoneRumbleEnabled = ControllerMappings.phoneRumbleEnabled()
|
||||||
// Push the saved haptic strength + achievement-sound volume into their native gates before
|
// Push the saved haptic strength + achievement-sound volume into their native gates before
|
||||||
// any rumble or unlock sound can fire (both default to 1.0 = as authored until set here).
|
// any rumble or unlock sound can fire (both default to 1.0 = as authored until set here).
|
||||||
ControllerMappings.syncHapticIntensity()
|
ControllerMappings.syncHapticIntensity()
|
||||||
|
|||||||
@@ -260,6 +260,16 @@ fun PadTab(state: MutableState<Settings>) {
|
|||||||
ControllerMappings.setRumbleEnabled(it)
|
ControllerMappings.setRumbleEnabled(it)
|
||||||
refreshToken.intValue++
|
refreshToken.intValue++
|
||||||
}
|
}
|
||||||
|
// Rumble prefers a connected controller's motor; this gates only the phone fallback,
|
||||||
|
// so playing on a pad need not mean the phone buzzes along with it.
|
||||||
|
ToggleRow(
|
||||||
|
str("pad.rumblePhone.label"),
|
||||||
|
ControllerMappings.phoneRumbleEnabled(),
|
||||||
|
description = str("pad.rumblePhone.description"),
|
||||||
|
) {
|
||||||
|
ControllerMappings.setPhoneRumbleEnabled(it)
|
||||||
|
refreshToken.intValue++
|
||||||
|
}
|
||||||
// Vibration strength: one multiplier over BOTH controller rumble and on-screen touch
|
// Vibration strength: one multiplier over BOTH controller rumble and on-screen touch
|
||||||
// haptics (they share the motor path), so an over-eager motor can be tamed or a weak
|
// haptics (they share the motor path), so an over-eager motor can be tamed or a weak
|
||||||
// one boosted. 100% = as authored; 0% = off.
|
// one boosted. 100% = as authored; 0% = off.
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
|||||||
SettingsSearchEntry("pad.section.playerRumble", true, SettingsCategory.Controls),
|
SettingsSearchEntry("pad.section.playerRumble", true, SettingsCategory.Controls),
|
||||||
SettingsSearchEntry("pad.editing.label", true, SettingsCategory.Controls),
|
SettingsSearchEntry("pad.editing.label", true, SettingsCategory.Controls),
|
||||||
SettingsSearchEntry("pad.rumble.label", true, SettingsCategory.Controls),
|
SettingsSearchEntry("pad.rumble.label", true, SettingsCategory.Controls),
|
||||||
|
SettingsSearchEntry("pad.rumblePhone.label", true, SettingsCategory.Controls),
|
||||||
SettingsSearchEntry("pad.hapticStrength.label", true, SettingsCategory.Controls),
|
SettingsSearchEntry("pad.hapticStrength.label", true, SettingsCategory.Controls),
|
||||||
SettingsSearchEntry("pad.pressureAmount.label", true, SettingsCategory.Controls),
|
SettingsSearchEntry("pad.pressureAmount.label", true, SettingsCategory.Controls),
|
||||||
SettingsSearchEntry("pad.section.analogSticks", true, SettingsCategory.Controls),
|
SettingsSearchEntry("pad.section.analogSticks", true, SettingsCategory.Controls),
|
||||||
|
|||||||
@@ -247,6 +247,13 @@ public final class NativeApp {
|
|||||||
/** Master rumble toggle. */
|
/** Master rumble toggle. */
|
||||||
public static volatile boolean sRumbleEnabled = true;
|
public static volatile boolean sRumbleEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the PHONE's own motor may be used. A controller's motor is always allowed; this
|
||||||
|
* only gates the fallback, so a user playing on a pad can stop the phone buzzing in their
|
||||||
|
* pocket or dock without giving up rumble entirely (issue #89). Default on.
|
||||||
|
*/
|
||||||
|
public static volatile boolean sPhoneRumbleEnabled = true;
|
||||||
|
|
||||||
/** Volume applied to UI sounds played through NativeApp.playSound. */
|
/** Volume applied to UI sounds played through NativeApp.playSound. */
|
||||||
public static volatile float sSoundVolume = 1.0f;
|
public static volatile float sSoundVolume = 1.0f;
|
||||||
|
|
||||||
|
|||||||
@@ -1390,7 +1390,8 @@ object Rpcs3Bridge {
|
|||||||
// called. Two stores, neither connected to the other.
|
// called. Two stores, neither connected to the other.
|
||||||
private val rumbleEnabled: Boolean get() = NativeApp.sRumbleEnabled
|
private val rumbleEnabled: Boolean get() = NativeApp.sRumbleEnabled
|
||||||
|
|
||||||
private fun vibrator(): Vibrator? {
|
/** The phone's own motor. */
|
||||||
|
private fun deviceVibrator(): Vibrator? {
|
||||||
val ctx = appContext ?: return null
|
val ctx = appContext ?: return null
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
(ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
|
(ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
|
||||||
@@ -1400,6 +1401,50 @@ object Rpcs3Bridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The motor in a connected controller, if one has it.
|
||||||
|
*
|
||||||
|
* Rumble went to the PHONE even with a controller attached, because this only ever asked the
|
||||||
|
* system service. The pad is what the game is addressing; the phone buzzing in its place is
|
||||||
|
* wrong, and on a handheld it is the wrong motor entirely (issue #89).
|
||||||
|
*
|
||||||
|
* The first gamepad or joystick with a working motor wins, in InputDevice id order, so a
|
||||||
|
* single connected pad is unambiguous.
|
||||||
|
*/
|
||||||
|
private fun controllerVibrator(): Vibrator? = runCatching {
|
||||||
|
for (id in android.view.InputDevice.getDeviceIds()) {
|
||||||
|
val dev = android.view.InputDevice.getDevice(id) ?: continue
|
||||||
|
|
||||||
|
val isPad = (dev.sources and android.view.InputDevice.SOURCE_GAMEPAD) ==
|
||||||
|
android.view.InputDevice.SOURCE_GAMEPAD ||
|
||||||
|
(dev.sources and android.view.InputDevice.SOURCE_JOYSTICK) ==
|
||||||
|
android.view.InputDevice.SOURCE_JOYSTICK
|
||||||
|
|
||||||
|
if (!isPad) continue
|
||||||
|
|
||||||
|
val v = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
dev.vibratorManager?.defaultVibrator
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
dev.vibrator
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v != null && v.hasVibrator()) return@runCatching v
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where rumble goes: the controller if one can take it, otherwise the phone.
|
||||||
|
*
|
||||||
|
* Resolved on demand rather than cached, because a pad can be connected or disconnected
|
||||||
|
* mid-session. Callers only ask when the motor state changes, so enumerating devices is not a
|
||||||
|
* per-frame cost.
|
||||||
|
*/
|
||||||
|
private fun vibrator(): Vibrator? =
|
||||||
|
controllerVibrator() ?: deviceVibrator().takeIf { NativeApp.sPhoneRumbleEnabled }
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setPadVibration(on: Boolean) {
|
fun setPadVibration(on: Boolean) {
|
||||||
NativeApp.sRumbleEnabled = on
|
NativeApp.sRumbleEnabled = on
|
||||||
@@ -1410,12 +1455,13 @@ object Rpcs3Bridge {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun startRumblePump() {
|
fun startRumblePump() {
|
||||||
if (rumbleRunning) return
|
if (rumbleRunning) return
|
||||||
val vib = vibrator() ?: return
|
|
||||||
if (!vib.hasVibrator()) return
|
|
||||||
|
|
||||||
rumbleRunning = true
|
rumbleRunning = true
|
||||||
rumbleThread = Thread {
|
rumbleThread = Thread {
|
||||||
var lastAmplitude = 0
|
var lastAmplitude = 0
|
||||||
|
// The motor we last started, so it can be stopped even if the target has since
|
||||||
|
// changed underneath us -- otherwise unplugging a pad mid-rumble leaves it buzzing.
|
||||||
|
var active: Vibrator? = null
|
||||||
while (rumbleRunning) {
|
while (rumbleRunning) {
|
||||||
val packed = runCatching { RPCSX.instance.getPadRumble(0) }.getOrDefault(0)
|
val packed = runCatching { RPCSX.instance.getPadRumble(0) }.getOrDefault(0)
|
||||||
val large = (packed shr 8) and 0xFF
|
val large = (packed shr 8) and 0xFF
|
||||||
@@ -1428,8 +1474,15 @@ object Rpcs3Bridge {
|
|||||||
|
|
||||||
if (want != lastAmplitude) {
|
if (want != lastAmplitude) {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (want <= 0) {
|
// Resolved per change, not once at startup: a pad connected mid-session
|
||||||
vib.cancel()
|
// has to take over from the phone, and vice versa on disconnect.
|
||||||
|
val vib = vibrator()
|
||||||
|
|
||||||
|
active?.takeIf { it !== vib }?.cancel()
|
||||||
|
active = null
|
||||||
|
|
||||||
|
if (want <= 0 || vib == null) {
|
||||||
|
vib?.cancel()
|
||||||
} else {
|
} else {
|
||||||
// Repeating one-shot rather than a fixed duration: the guest
|
// Repeating one-shot rather than a fixed duration: the guest
|
||||||
// decides when rumble stops, and a timed effect would either cut
|
// decides when rumble stops, and a timed effect would either cut
|
||||||
@@ -1439,6 +1492,7 @@ object Rpcs3Bridge {
|
|||||||
longArrayOf(0, 60), intArrayOf(0, want.coerceIn(1, 255)), 0
|
longArrayOf(0, 60), intArrayOf(0, want.coerceIn(1, 255)), 0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
active = vib
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastAmplitude = want
|
lastAmplitude = want
|
||||||
@@ -1446,7 +1500,7 @@ object Rpcs3Bridge {
|
|||||||
|
|
||||||
try { Thread.sleep(30) } catch (_: InterruptedException) { break }
|
try { Thread.sleep(30) } catch (_: InterruptedException) { break }
|
||||||
}
|
}
|
||||||
runCatching { vib.cancel() }
|
runCatching { active?.cancel() }
|
||||||
}.apply { isDaemon = true; name = "rumble-pump"; start() }
|
}.apply { isDaemon = true; name = "rumble-pump"; start() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1500,13 +1554,9 @@ object Rpcs3Bridge {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun touchHaptic() {
|
fun touchHaptic() {
|
||||||
val ctx = appContext ?: return
|
// The phone's motor deliberately, not vibrator(): this is feedback for a finger on the
|
||||||
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
// phone's own screen, so it belongs there even when a controller is holding the rumble.
|
||||||
(ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
|
val vibrator = deviceVibrator() ?: return
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
ctx.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
||||||
} ?: return
|
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
vibrator.vibrate(VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE))
|
vibrator.vibrate(VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
|
|||||||
Reference in New Issue
Block a user