Android: Multiple fixes

- Fix possible crash when applying settings worker thread (no JNIEnv).
 - Fix settings not applying until restarting the app.
 - Support analog controller - auto-binding of axixes. Currently no
   touchscreen controller for the joysticks.
 - Add option to auto-hide the touchscreen controller.
This commit is contained in:
Connor McLaughlin 2020-07-29 02:24:25 +10:00
parent c7b457de9e
commit 24ffe6f67e
10 changed files with 183 additions and 75 deletions

View File

@ -3,7 +3,6 @@ set(SRCS
android_host_interface.h android_host_interface.h
android_settings_interface.cpp android_settings_interface.cpp
android_settings_interface.h android_settings_interface.h
main.cpp
) )
add_library(duckstation-native SHARED ${SRCS}) add_library(duckstation-native SHARED ${SRCS})

View File

@ -11,6 +11,7 @@
#include "core/system.h" #include "core/system.h"
#include "frontend-common/opengl_host_display.h" #include "frontend-common/opengl_host_display.h"
#include "frontend-common/vulkan_host_display.h" #include "frontend-common/vulkan_host_display.h"
#include "frontend-common/imgui_styles.h"
#include <android/native_window_jni.h> #include <android/native_window_jni.h>
#include <cmath> #include <cmath>
#include <imgui.h> #include <imgui.h>
@ -195,8 +196,18 @@ void AndroidHostInterface::RunOnEmulationThread(std::function<void()> function,
void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surface, SystemBootParameters boot_params) void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surface, SystemBootParameters boot_params)
{ {
JNIEnv* thread_env;
if (s_jvm->AttachCurrentThread(&thread_env, nullptr) != JNI_OK)
{
Log_ErrorPrintf("Failed to attach JNI to thread");
m_emulation_thread_start_result.store(false);
m_emulation_thread_started.Signal();
return;
}
CreateImGuiContext(); CreateImGuiContext();
m_surface = initial_surface; m_surface = initial_surface;
ApplySettings();
// Boot system. // Boot system.
if (!BootSystem(boot_params)) if (!BootSystem(boot_params))
@ -205,6 +216,7 @@ void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surf
DestroyImGuiContext(); DestroyImGuiContext();
m_emulation_thread_start_result.store(false); m_emulation_thread_start_result.store(false);
m_emulation_thread_started.Signal(); m_emulation_thread_started.Signal();
s_jvm->DetachCurrentThread();
return; return;
} }
@ -256,6 +268,7 @@ void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surf
DestroySystem(); DestroySystem();
DestroyImGuiContext(); DestroyImGuiContext();
s_jvm->DetachCurrentThread();
} }
bool AndroidHostInterface::AcquireHostDisplay() bool AndroidHostInterface::AcquireHostDisplay()
@ -297,31 +310,6 @@ void AndroidHostInterface::ReleaseHostDisplay()
m_display.reset(); m_display.reset();
} }
std::unique_ptr<AudioStream> AndroidHostInterface::CreateAudioStream(AudioBackend backend)
{
std::unique_ptr<AudioStream> stream;
switch (m_settings.audio_backend)
{
case AudioBackend::Cubeb:
stream = AudioStream::CreateCubebAudioStream();
break;
default:
stream = AudioStream::CreateNullAudioStream();
break;
}
if (!stream)
{
ReportFormattedError("Failed to create %s audio stream, falling back to null",
Settings::GetAudioBackendName(m_settings.audio_backend));
stream = AudioStream::CreateNullAudioStream();
}
return stream;
}
void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, int width, int height) void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, int width, int height)
{ {
Log_InfoPrintf("SurfaceChanged %p %d %d %d", surface, format, width, height); Log_InfoPrintf("SurfaceChanged %p %d %d %d", surface, format, width, height);
@ -351,9 +339,16 @@ void AndroidHostInterface::CreateImGuiContext()
{ {
ImGui::CreateContext(); ImGui::CreateContext();
ImGui::GetIO().IniFilename = nullptr; const float framebuffer_scale = 2.0f;
// ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
// ImGui::GetIO().BackendFlags |= ImGuiBackendFlags_HasGamepad; auto& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.DisplayFramebufferScale.x = framebuffer_scale;
io.DisplayFramebufferScale.y = framebuffer_scale;
ImGui::GetStyle().ScaleAllSizes(framebuffer_scale);
ImGui::StyleColorsDarker();
ImGui::AddRobotoRegularFont(15.0f * framebuffer_scale);
} }
void AndroidHostInterface::DestroyImGuiContext() void AndroidHostInterface::DestroyImGuiContext()
@ -397,6 +392,22 @@ void AndroidHostInterface::SetControllerButtonState(u32 index, s32 button_code,
false); false);
} }
void AndroidHostInterface::SetControllerAxisState(u32 index, s32 button_code, float value)
{
if (!IsEmulationThreadRunning())
return;
RunOnEmulationThread(
[this, index, button_code, value]() {
Controller* controller = m_system->GetController(index);
if (!controller)
return;
controller->SetAxisState(button_code, value);
},
false);
}
void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidate_database) void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidate_database)
{ {
m_game_list->SetSearchDirectoriesFromSettings(m_settings_interface); m_game_list->SetSearchDirectoriesFromSettings(m_settings_interface);
@ -544,6 +555,25 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerButtonCode, jobje
return code.value_or(-1); return code.value_or(-1);
} }
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setControllerAxisState, jobject obj, jint index, jint button_code,
jfloat value)
{
AndroidHelpers::GetNativeClass(env, obj)->SetControllerAxisState(index, button_code, value);
}
DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisCode, jobject unused, jstring controller_type,
jstring axis_name)
{
std::optional<ControllerType> type =
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
if (!type)
return -1;
std::optional<s32> code =
Controller::GetAxisCodeByName(type.value(), AndroidHelpers::JStringToString(env, axis_name));
return code.value_or(-1);
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache, jboolean invalidate_database) DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache, jboolean invalidate_database)
{ {
AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database); AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database);

View File

@ -43,6 +43,7 @@ public:
void SetControllerType(u32 index, std::string_view type_name); void SetControllerType(u32 index, std::string_view type_name);
void SetControllerButtonState(u32 index, s32 button_code, bool pressed); void SetControllerButtonState(u32 index, s32 button_code, bool pressed);
void SetControllerAxisState(u32 index, s32 button_code, float value);
void RefreshGameList(bool invalidate_cache, bool invalidate_database); void RefreshGameList(bool invalidate_cache, bool invalidate_database);
void ApplySettings(); void ApplySettings();
@ -54,7 +55,6 @@ protected:
bool AcquireHostDisplay() override; bool AcquireHostDisplay() override;
void ReleaseHostDisplay() override; void ReleaseHostDisplay() override;
std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
private: private:
void EmulationThreadEntryPoint(ANativeWindow* initial_surface, SystemBootParameters boot_params); void EmulationThreadEntryPoint(ANativeWindow* initial_surface, SystemBootParameters boot_params);

View File

@ -1,17 +0,0 @@
#include "core/host_interface.h"
#include <jni.h>
#define DEFINE_JNI_METHOD(return_type, name, ...) \
extern "C" JNIEXPORT return_type JNICALL Java_com_github_stenzek_duckstation_##name(__VA_ARGS__)
DEFINE_JNI_METHOD(bool, createSystem)
{
return false;
}
DEFINE_JNI_METHOD(bool, bootSystem, const char* filename, const char* state_filename)
{
return false;
}
DEFINE_JNI_METHOD(void, runFrame) {}

View File

@ -23,7 +23,9 @@ public class AndroidHostInterface
// TODO: Find a better place for this. // TODO: Find a better place for this.
public native void setControllerType(int index, String typeName); public native void setControllerType(int index, String typeName);
public native void setControllerButtonState(int index, int buttonCode, boolean pressed); public native void setControllerButtonState(int index, int buttonCode, boolean pressed);
public native void setControllerAxisState(int index, int axisCode, float value);
public static native int getControllerButtonCode(String controllerType, String buttonName); public static native int getControllerButtonCode(String controllerType, String buttonName);
public static native int getControllerAxisCode(String controllerType, String axisName);
public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase); public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase);
public native GameListEntry[] getGameListEntries(); public native GameListEntry[] getGameListEntries();

View File

@ -38,6 +38,9 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
editor.putBoolean(key, value); editor.putBoolean(key, value);
editor.apply(); editor.apply();
} }
private String getStringSetting(String key, String defaultValue) {
return mPreferences.getString(key, defaultValue);
}
/** /**
* Touchscreen controller overlay * Touchscreen controller overlay
@ -154,15 +157,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
} }
}); });
// Hook up controller input.
final String controllerType = getStringSetting("Controller1/Type", "DigitalController");
Log.i("EmulationActivity", "Controller type: " + controllerType);
mContentView.initControllerKeyMapping(controllerType);
// Create touchscreen controller. // Create touchscreen controller.
FrameLayout activityLayout = findViewById(R.id.frameLayout); FrameLayout activityLayout = findViewById(R.id.frameLayout);
mTouchscreenController = new TouchscreenControllerView(this); mTouchscreenController = new TouchscreenControllerView(this);
activityLayout.addView(mTouchscreenController); activityLayout.addView(mTouchscreenController);
mTouchscreenController.init(0, "DigitalController", AndroidHostInterface.getInstance()); mTouchscreenController.init(0, controllerType);
setTouchscreenControllerVisibility(true); setTouchscreenControllerVisibility(getBooleanSetting("Controller1/EnableTouchscreenController", true));
// Hook up controller input.
mContentView.initControllerKeyMapping("DigitalController");
} }
@Override @Override
@ -181,6 +186,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
getMenuInflater().inflate(R.menu.menu_emulation, menu); getMenuInflater().inflate(R.menu.menu_emulation, menu);
menu.findItem(R.id.show_controller).setChecked(mTouchscreenControllerVisible); menu.findItem(R.id.show_controller).setChecked(mTouchscreenControllerVisible);
menu.findItem(R.id.enable_speed_limiter).setChecked(getBooleanSetting("Main/SpeedLimiterEnabled", true)); menu.findItem(R.id.enable_speed_limiter).setChecked(getBooleanSetting("Main/SpeedLimiterEnabled", true));
menu.findItem(R.id.show_controller).setChecked(getBooleanSetting("Controller1/EnableTouchscreenController", true));
return true; return true;
} }

View File

@ -4,8 +4,10 @@ import android.content.Context;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceView; import android.view.SurfaceView;
public class EmulationSurfaceView extends SurfaceView { public class EmulationSurfaceView extends SurfaceView {
@ -48,7 +50,49 @@ public class EmulationSurfaceView extends SurfaceView {
return super.onKeyDown(keyCode, event); return super.onKeyDown(keyCode, event);
} }
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
final int source = event.getSource();
if ((source & InputDevice.SOURCE_JOYSTICK) == 0)
return super.onGenericMotionEvent(event);
final InputDevice device = event.getDevice();
for (int axis : AXISES) {
Integer mapping = mControllerAxisMapping.containsKey(axis) ? mControllerAxisMapping.get(axis) : null;
Pair<Integer, Integer> buttonMapping = mControllerAxisButtonMapping.containsKey(axis) ? mControllerAxisButtonMapping.get(axis) : null;
if (mapping == null && buttonMapping == null)
continue;
final float axisValue = event.getAxisValue(axis);
float emuValue;
final InputDevice.MotionRange range = device.getMotionRange(axis, source);
if (range != null) {
final float transformedValue = (axisValue - range.getMin()) / range.getRange();
emuValue = (transformedValue * 2.0f) - 1.0f;
} else {
emuValue = axisValue;
}
Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue));
if (mapping != null) {
AndroidHostInterface.getInstance().setControllerAxisState(0, mapping, emuValue);
} else {
final float DEAD_ZONE = 0.25f;
AndroidHostInterface.getInstance().setControllerButtonState(0, buttonMapping.first, (emuValue <= -DEAD_ZONE));
AndroidHostInterface.getInstance().setControllerButtonState(0, buttonMapping.second, (emuValue >= DEAD_ZONE));
Log.d("EmulationSurfaceView", String.format("using emuValue %f for buttons %d %d", emuValue, buttonMapping.first, buttonMapping.second));
}
}
return true;
}
private ArrayMap<Integer, Integer> mControllerKeyMapping; private ArrayMap<Integer, Integer> mControllerKeyMapping;
private ArrayMap<Integer, Integer> mControllerAxisMapping;
private ArrayMap<Integer, Pair<Integer, Integer>> mControllerAxisButtonMapping;
static final int[] AXISES = new int[]{MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_RX,
MotionEvent.AXIS_RY, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ,
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y};
private void addControllerKeyMapping(int keyCode, String controllerType, String buttonName) { private void addControllerKeyMapping(int keyCode, String controllerType, String buttonName) {
int mapping = AndroidHostInterface.getControllerButtonCode(controllerType, buttonName); int mapping = AndroidHostInterface.getControllerButtonCode(controllerType, buttonName);
@ -58,8 +102,31 @@ public class EmulationSurfaceView extends SurfaceView {
mControllerKeyMapping.put(keyCode, mapping); mControllerKeyMapping.put(keyCode, mapping);
} }
private void addControllerAxisMapping(int axis, String controllerType, String axisName, String negativeButtonName, String positiveButtonName) {
if (axisName != null) {
int mapping = AndroidHostInterface.getControllerAxisCode(controllerType, axisName);
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d (%s)", axis, mapping, axisName));
if (mapping >= 0) {
mControllerAxisMapping.put(axis, mapping);
return;
}
}
if (negativeButtonName != null && positiveButtonName != null) {
final int negativeMapping = AndroidHostInterface.getControllerButtonCode(controllerType, negativeButtonName);
final int positiveMapping = AndroidHostInterface.getControllerButtonCode(controllerType, positiveButtonName);
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d %d (button %s %s)", axis, negativeMapping, positiveMapping,
negativeButtonName, positiveButtonName));
if (negativeMapping >= 0 && positiveMapping >= 0) {
mControllerAxisButtonMapping.put(axis, new Pair<Integer, Integer>(negativeMapping, positiveMapping));
}
}
}
public void initControllerKeyMapping(String controllerType) { public void initControllerKeyMapping(String controllerType) {
mControllerKeyMapping = new ArrayMap<>(); mControllerKeyMapping = new ArrayMap<>();
mControllerAxisMapping = new ArrayMap<>();
mControllerAxisButtonMapping = new ArrayMap<>();
// TODO: Don't hardcode... // TODO: Don't hardcode...
addControllerKeyMapping(KeyEvent.KEYCODE_DPAD_UP, controllerType, "Up"); addControllerKeyMapping(KeyEvent.KEYCODE_DPAD_UP, controllerType, "Up");
@ -76,6 +143,14 @@ public class EmulationSurfaceView extends SurfaceView {
addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_X, controllerType, "Square"); addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_X, controllerType, "Square");
addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_R1, controllerType, "R1"); addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_R1, controllerType, "R1");
addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_R2, controllerType, "R2"); addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_R2, controllerType, "R2");
addControllerAxisMapping(MotionEvent.AXIS_X, controllerType, "LeftX", null, null);
addControllerAxisMapping(MotionEvent.AXIS_Y, controllerType, "LeftY", null, null);
addControllerAxisMapping(MotionEvent.AXIS_RX, controllerType, "RightX", null, null);
addControllerAxisMapping(MotionEvent.AXIS_RY, controllerType, "RightY", null, null);
addControllerAxisMapping(MotionEvent.AXIS_Z, controllerType, "L2", "L2", "L2");
addControllerAxisMapping(MotionEvent.AXIS_RZ, controllerType, "R2", "R2", "R2");
addControllerAxisMapping(MotionEvent.AXIS_HAT_X, controllerType, null, "Left", "Right");
addControllerAxisMapping(MotionEvent.AXIS_HAT_Y, controllerType, null, "Up", "Down");
} }
private boolean handleControllerKey(int keyCode, boolean pressed) { private boolean handleControllerKey(int keyCode, boolean pressed) {
@ -84,6 +159,7 @@ public class EmulationSurfaceView extends SurfaceView {
final int mapping = mControllerKeyMapping.get(keyCode); final int mapping = mControllerKeyMapping.get(keyCode);
AndroidHostInterface.getInstance().setControllerButtonState(0, mapping, pressed); AndroidHostInterface.getInstance().setControllerButtonState(0, mapping, pressed);
Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping, pressed ? 1 : 0));
return true; return true;
} }
} }

View File

@ -13,7 +13,6 @@ import android.widget.FrameLayout;
public class TouchscreenControllerView extends FrameLayout implements TouchscreenControllerButtonView.ButtonStateChangedListener { public class TouchscreenControllerView extends FrameLayout implements TouchscreenControllerButtonView.ButtonStateChangedListener {
private int mControllerIndex; private int mControllerIndex;
private String mControllerType; private String mControllerType;
private AndroidHostInterface mHostInterface;
public TouchscreenControllerView(Context context) { public TouchscreenControllerView(Context context) {
super(context); super(context);
@ -27,14 +26,9 @@ public class TouchscreenControllerView extends FrameLayout implements Touchscree
super(context, attrs, defStyle); super(context, attrs, defStyle);
} }
public void init(int controllerIndex, String controllerType, public void init(int controllerIndex, String controllerType) {
AndroidHostInterface hostInterface) {
mControllerIndex = controllerIndex; mControllerIndex = controllerIndex;
mControllerType = controllerType; mControllerType = controllerType;
mHostInterface = hostInterface;
if (mHostInterface != null)
mHostInterface.setControllerType(controllerIndex, controllerType);
LayoutInflater inflater = LayoutInflater.from(getContext()); LayoutInflater inflater = LayoutInflater.from(getContext());
View view = inflater.inflate(R.layout.layout_touchscreen_controller, this, true); View view = inflater.inflate(R.layout.layout_touchscreen_controller, this, true);
@ -62,25 +56,22 @@ public class TouchscreenControllerView extends FrameLayout implements Touchscree
buttonView.setButtonName(buttonName); buttonView.setButtonName(buttonName);
buttonView.setButtonStateChangedListener(this); buttonView.setButtonStateChangedListener(this);
if (mHostInterface != null) int code = AndroidHostInterface.getInstance().getControllerButtonCode(mControllerType, buttonName);
{ buttonView.setButtonCode(code);
int code = mHostInterface.getControllerButtonCode(mControllerType, buttonName); Log.i("TouchscreenController", String.format("%s -> %d", buttonName, code));
buttonView.setButtonCode(code);
Log.i("TouchscreenController", String.format("%s -> %d", buttonName, code));
if (code < 0) { if (code < 0) {
Log.e("TouchscreenController", String.format("Unknown button name '%s' " + Log.e("TouchscreenController", String.format("Unknown button name '%s' " +
"for '%s'", buttonName, mControllerType)); "for '%s'", buttonName, mControllerType));
}
} }
} }
@Override @Override
public void onButtonStateChanged(TouchscreenControllerButtonView view, boolean pressed) { public void onButtonStateChanged(TouchscreenControllerButtonView view, boolean pressed) {
if (mHostInterface == null || view.getButtonCode() < 0) if (view.getButtonCode() < 0)
return; return;
mHostInterface.setControllerButtonState(mControllerIndex, view.getButtonCode(), pressed); AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex, view.getButtonCode(), pressed);
} }
} }

View File

@ -67,4 +67,12 @@
<item>15</item> <item>15</item>
<item>16</item> <item>16</item>
</string-array> </string-array>
<string-array name="settings_controller_type_entries">
<item>Digital Controller (Gamepad)</item>
<item>Analog Controller (DualShock)</item>
</string-array>
<string-array name="settings_controller_type_values">
<item>DigitalController</item>
<item>AnalogController</item>
</string-array>
</resources> </resources>

View File

@ -41,11 +41,6 @@
app:defaultValue="@string/settings_console_region_default" app:defaultValue="@string/settings_console_region_default"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<EditTextPreference
app:key="BIOS/Path"
app:title="@string/settings_console_bios_path"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="BIOS/PatchTTYEnable" app:key="BIOS/PatchTTYEnable"
app:title="@string/settings_console_tty_output" app:title="@string/settings_console_tty_output"
@ -144,4 +139,22 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="Controller">
<ListPreference
app:key="Controller1/Type"
app:title="Controller Type"
app:entries="@array/settings_controller_type_entries"
app:entryValues="@array/settings_controller_type_values"
app:defaultValue="DigitalController"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="Controller1/AutoEnableAnalog"
app:title="Enable Analog Mode On Reset"
app:defaultValue="false" />
<SwitchPreferenceCompat
app:key="Controller1/EnableTouchscreenController"
app:title="Display Touchscreen Controller"
app:defaultValue="true" />
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>