Android: Handle duplicate axis, and fix known bad axis.

Android reports the same physical axis multiple times for analog
triggers, and this handles this case.

There are also some controllers with broken mappings (eg the analog
triggers on a PS4 DualShock 4). These axis don't center correctly.

There are also some controllers (again, the PS4) that send both a button
press and an axis movement. This ignores the buttons so we can use the
analog axis. Otherwise, since the button comes before the axis moves
far we would always take the button.
This commit is contained in:
Mike 2017-10-31 22:43:30 -07:00
parent d9d4bd7eef
commit 6787fcb712
3 changed files with 96 additions and 9 deletions

View File

@ -39,6 +39,7 @@ import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment;
import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
import org.dolphinemu.dolphinemu.ui.platform.Platform; import org.dolphinemu.dolphinemu.ui.platform.Platform;
import org.dolphinemu.dolphinemu.utils.Animations; import org.dolphinemu.dolphinemu.utils.Animations;
import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper;
import org.dolphinemu.dolphinemu.utils.Java_GCAdapter; import org.dolphinemu.dolphinemu.utils.Java_GCAdapter;
import org.dolphinemu.dolphinemu.utils.Java_WiimoteAdapter; import org.dolphinemu.dolphinemu.utils.Java_WiimoteAdapter;
import org.dolphinemu.dolphinemu.utils.Log; import org.dolphinemu.dolphinemu.utils.Log;
@ -57,6 +58,7 @@ public final class EmulationActivity extends AppCompatActivity
private EmulationFragment mEmulationFragment; private EmulationFragment mEmulationFragment;
private SharedPreferences mPreferences; private SharedPreferences mPreferences;
private ControllerMappingHelper mControllerMappingHelper;
// So that MainActivity knows which view to invalidate before the return animation. // So that MainActivity knows which view to invalidate before the return animation.
private int mPosition; private int mPosition;
@ -164,6 +166,7 @@ public final class EmulationActivity extends AppCompatActivity
mScreenPath = gameToEmulate.getStringExtra("ScreenPath"); mScreenPath = gameToEmulate.getStringExtra("ScreenPath");
mPosition = gameToEmulate.getIntExtra("GridPosition", -1); mPosition = gameToEmulate.getIntExtra("GridPosition", -1);
mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen"); mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen");
mControllerMappingHelper = new ControllerMappingHelper();
int themeId; int themeId;
if (mDeviceHasTouchScreen) if (mDeviceHasTouchScreen)
@ -729,7 +732,10 @@ public final class EmulationActivity extends AppCompatActivity
for (InputDevice.MotionRange range : motions) for (InputDevice.MotionRange range : motions)
{ {
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), range.getAxis(), event.getAxisValue(range.getAxis())); int axis = range.getAxis();
float origValue = event.getAxisValue(axis);
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), axis, value);
} }
return true; return true;

View File

@ -9,6 +9,7 @@ import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import org.dolphinemu.dolphinemu.model.settings.view.InputBindingSetting; import org.dolphinemu.dolphinemu.model.settings.view.InputBindingSetting;
import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper;
import org.dolphinemu.dolphinemu.utils.Log; import org.dolphinemu.dolphinemu.utils.Log;
import java.util.List; import java.util.List;
@ -21,6 +22,7 @@ public final class MotionAlertDialog extends AlertDialog
{ {
// The selected input preference // The selected input preference
private final InputBindingSetting setting; private final InputBindingSetting setting;
private final ControllerMappingHelper mControllerMappingHelper;
private boolean mWaitingForEvent = true; private boolean mWaitingForEvent = true;
/** /**
@ -34,6 +36,7 @@ public final class MotionAlertDialog extends AlertDialog
super(context); super(context);
this.setting = setting; this.setting = setting;
this.mControllerMappingHelper = new ControllerMappingHelper();
} }
public boolean onKeyEvent(int keyCode, KeyEvent event) public boolean onKeyEvent(int keyCode, KeyEvent event)
@ -42,8 +45,11 @@ public final class MotionAlertDialog extends AlertDialog
switch (event.getAction()) switch (event.getAction())
{ {
case KeyEvent.ACTION_DOWN: case KeyEvent.ACTION_DOWN:
if (!mControllerMappingHelper.shouldKeyBeIgnored(event.getDevice(), keyCode))
{
saveKeyInput(event); saveKeyInput(event);
}
// Even if we ignore the key, we still consume it. Thus return true regardless.
return true; return true;
default: default:
@ -69,13 +75,15 @@ public final class MotionAlertDialog extends AlertDialog
{ {
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
return false; return false;
if (event.getAction() != MotionEvent.ACTION_MOVE)
Log.debug("[MotionAlertDialog] Received motion event: " + event.getAction()); return false;
InputDevice input = event.getDevice(); InputDevice input = event.getDevice();
List<InputDevice.MotionRange> motionRanges = input.getMotionRanges(); List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
int numMovedAxis = 0; int numMovedAxis = 0;
float axisMoveValue = 0.0f;
InputDevice.MotionRange lastMovedRange = null; InputDevice.MotionRange lastMovedRange = null;
char lastMovedDir = '?'; char lastMovedDir = '?';
if (mWaitingForEvent) if (mWaitingForEvent)
@ -84,14 +92,25 @@ public final class MotionAlertDialog extends AlertDialog
for (InputDevice.MotionRange range : motionRanges) for (InputDevice.MotionRange range : motionRanges)
{ {
int axis = range.getAxis(); int axis = range.getAxis();
float value = event.getAxisValue(axis); float origValue = event.getAxisValue(axis);
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
if (Math.abs(value) > 0.5f) if (Math.abs(value) > 0.5f)
{ {
// It is common to have multiple axis with the same physical input. For example,
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
// To handle this, we ignore an axis motion that's the exact same as a motion
// we already saw. This way, we ignore axis with two names, but catch the case
// where a joystick is moved in two directions.
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
if (value != axisMoveValue)
{
axisMoveValue = value;
numMovedAxis++; numMovedAxis++;
lastMovedRange = range; lastMovedRange = range;
lastMovedDir = value < 0.0f ? '-' : '+'; lastMovedDir = value < 0.0f ? '-' : '+';
} }
} }
}
// If only one axis moved, that's the winner. // If only one axis moved, that's the winner.
if (numMovedAxis == 1) if (numMovedAxis == 1)

View File

@ -0,0 +1,62 @@
package org.dolphinemu.dolphinemu.utils;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
/** Some controllers have incorrect mappings. This class has special-case fixes for them. */
public class ControllerMappingHelper
{
/** Some controllers report extra button presses that can be ignored. */
public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode)
{
if (isDualShock4(inputDevice)) {
// The two analog triggers generate analog motion events as well as a keycode.
// We always prefer to use the analog values, so throw away the button press
// Even though the triggers are L/R2, without mappings they generate L/R1 events.
return keyCode == KeyEvent.KEYCODE_BUTTON_L1 || keyCode == KeyEvent.KEYCODE_BUTTON_R1;
}
return false;
}
/** Scale an axis to be zero-centered with a proper range. */
public float scaleAxis(InputDevice inputDevice, int axis, float value)
{
if (isDualShock4(inputDevice))
{
// Android doesn't have correct mappings for this controller's triggers. It reports them
// as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
// Scale them to properly zero-centered with a range of [0.0, 1.0].
if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY)
{
return (value + 1) / 2.0f;
}
}
else if (isXboxOneWireless(inputDevice))
{
// Same as the DualShock 4, the mappings are missing.
if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ)
{
return (value + 1) / 2.0f;
}
if (axis == MotionEvent.AXIS_GENERIC_1)
{
// This axis is stuck at ~.5. Ignore it.
return 0.0f;
}
}
return value;
}
private boolean isDualShock4(InputDevice inputDevice)
{
// Sony DualShock 4 controller
return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc;
}
private boolean isXboxOneWireless(InputDevice inputDevice)
{
// Microsoft Xbox One controller
return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0;
}
}