From 24ffe6f67e6f1895c128270d7f89259e19992ea5 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Wed, 29 Jul 2020 02:24:25 +1000 Subject: [PATCH] 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. --- android/app/src/cpp/CMakeLists.txt | 1 - .../app/src/cpp/android_host_interface.cpp | 86 +++++++++++++------ android/app/src/cpp/android_host_interface.h | 2 +- android/app/src/cpp/main.cpp | 17 ---- .../duckstation/AndroidHostInterface.java | 2 + .../duckstation/EmulationActivity.java | 16 ++-- .../duckstation/EmulationSurfaceView.java | 76 ++++++++++++++++ .../TouchscreenControllerView.java | 27 ++---- android/app/src/main/res/values/arrays.xml | 8 ++ .../app/src/main/res/xml/root_preferences.xml | 23 +++-- 10 files changed, 183 insertions(+), 75 deletions(-) delete mode 100644 android/app/src/cpp/main.cpp diff --git a/android/app/src/cpp/CMakeLists.txt b/android/app/src/cpp/CMakeLists.txt index d8b1d9d3f..94f6e71ea 100644 --- a/android/app/src/cpp/CMakeLists.txt +++ b/android/app/src/cpp/CMakeLists.txt @@ -3,7 +3,6 @@ set(SRCS android_host_interface.h android_settings_interface.cpp android_settings_interface.h - main.cpp ) add_library(duckstation-native SHARED ${SRCS}) diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 17098ee13..5b9047365 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -11,6 +11,7 @@ #include "core/system.h" #include "frontend-common/opengl_host_display.h" #include "frontend-common/vulkan_host_display.h" +#include "frontend-common/imgui_styles.h" #include #include #include @@ -195,8 +196,18 @@ void AndroidHostInterface::RunOnEmulationThread(std::function function, 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(); m_surface = initial_surface; + ApplySettings(); // Boot system. if (!BootSystem(boot_params)) @@ -205,6 +216,7 @@ void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surf DestroyImGuiContext(); m_emulation_thread_start_result.store(false); m_emulation_thread_started.Signal(); + s_jvm->DetachCurrentThread(); return; } @@ -256,6 +268,7 @@ void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surf DestroySystem(); DestroyImGuiContext(); + s_jvm->DetachCurrentThread(); } bool AndroidHostInterface::AcquireHostDisplay() @@ -297,31 +310,6 @@ void AndroidHostInterface::ReleaseHostDisplay() m_display.reset(); } -std::unique_ptr AndroidHostInterface::CreateAudioStream(AudioBackend backend) -{ - std::unique_ptr 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) { Log_InfoPrintf("SurfaceChanged %p %d %d %d", surface, format, width, height); @@ -351,9 +339,16 @@ void AndroidHostInterface::CreateImGuiContext() { ImGui::CreateContext(); - ImGui::GetIO().IniFilename = nullptr; - // ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; - // ImGui::GetIO().BackendFlags |= ImGuiBackendFlags_HasGamepad; + const float framebuffer_scale = 2.0f; + + 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() @@ -397,6 +392,22 @@ void AndroidHostInterface::SetControllerButtonState(u32 index, s32 button_code, 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) { m_game_list->SetSearchDirectoriesFromSettings(m_settings_interface); @@ -544,6 +555,25 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerButtonCode, jobje 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 type = + Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); + if (!type) + return -1; + + std::optional 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) { AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database); diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index 54820675c..53432cfb5 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -43,6 +43,7 @@ public: void SetControllerType(u32 index, std::string_view type_name); 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 ApplySettings(); @@ -54,7 +55,6 @@ protected: bool AcquireHostDisplay() override; void ReleaseHostDisplay() override; - std::unique_ptr CreateAudioStream(AudioBackend backend) override; private: void EmulationThreadEntryPoint(ANativeWindow* initial_surface, SystemBootParameters boot_params); diff --git a/android/app/src/cpp/main.cpp b/android/app/src/cpp/main.cpp deleted file mode 100644 index 4ba74ea11..000000000 --- a/android/app/src/cpp/main.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "core/host_interface.h" -#include - -#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) {} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index 2488e695c..5899a7a85 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -23,7 +23,9 @@ public class AndroidHostInterface // TODO: Find a better place for this. public native void setControllerType(int index, String typeName); 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 getControllerAxisCode(String controllerType, String axisName); public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase); public native GameListEntry[] getGameListEntries(); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index 15756551c..8497145ec 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -38,6 +38,9 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde editor.putBoolean(key, value); editor.apply(); } + private String getStringSetting(String key, String defaultValue) { + return mPreferences.getString(key, defaultValue); + } /** * 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. FrameLayout activityLayout = findViewById(R.id.frameLayout); mTouchscreenController = new TouchscreenControllerView(this); activityLayout.addView(mTouchscreenController); - mTouchscreenController.init(0, "DigitalController", AndroidHostInterface.getInstance()); - setTouchscreenControllerVisibility(true); - - // Hook up controller input. - mContentView.initControllerKeyMapping("DigitalController"); + mTouchscreenController.init(0, controllerType); + setTouchscreenControllerVisibility(getBooleanSetting("Controller1/EnableTouchscreenController", true)); } @Override @@ -181,6 +186,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde getMenuInflater().inflate(R.menu.menu_emulation, menu); menu.findItem(R.id.show_controller).setChecked(mTouchscreenControllerVisible); 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; } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java index 5a879d947..cf90f881e 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java @@ -4,8 +4,10 @@ import android.content.Context; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.InputDevice; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.SurfaceView; public class EmulationSurfaceView extends SurfaceView { @@ -48,7 +50,49 @@ public class EmulationSurfaceView extends SurfaceView { 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 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 mControllerKeyMapping; + private ArrayMap mControllerAxisMapping; + private ArrayMap> 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) { int mapping = AndroidHostInterface.getControllerButtonCode(controllerType, buttonName); @@ -58,8 +102,31 @@ public class EmulationSurfaceView extends SurfaceView { 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(negativeMapping, positiveMapping)); + } + } + } + public void initControllerKeyMapping(String controllerType) { mControllerKeyMapping = new ArrayMap<>(); + mControllerAxisMapping = new ArrayMap<>(); + mControllerAxisButtonMapping = new ArrayMap<>(); // TODO: Don't hardcode... 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_R1, controllerType, "R1"); 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) { @@ -84,6 +159,7 @@ public class EmulationSurfaceView extends SurfaceView { final int mapping = mControllerKeyMapping.get(keyCode); AndroidHostInterface.getInstance().setControllerButtonState(0, mapping, pressed); + Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping, pressed ? 1 : 0)); return true; } } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java index fb7abbe0f..0c43b379e 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/TouchscreenControllerView.java @@ -13,7 +13,6 @@ import android.widget.FrameLayout; public class TouchscreenControllerView extends FrameLayout implements TouchscreenControllerButtonView.ButtonStateChangedListener { private int mControllerIndex; private String mControllerType; - private AndroidHostInterface mHostInterface; public TouchscreenControllerView(Context context) { super(context); @@ -27,14 +26,9 @@ public class TouchscreenControllerView extends FrameLayout implements Touchscree super(context, attrs, defStyle); } - public void init(int controllerIndex, String controllerType, - AndroidHostInterface hostInterface) { + public void init(int controllerIndex, String controllerType) { mControllerIndex = controllerIndex; mControllerType = controllerType; - mHostInterface = hostInterface; - - if (mHostInterface != null) - mHostInterface.setControllerType(controllerIndex, controllerType); LayoutInflater inflater = LayoutInflater.from(getContext()); 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.setButtonStateChangedListener(this); - if (mHostInterface != null) - { - int code = mHostInterface.getControllerButtonCode(mControllerType, buttonName); - buttonView.setButtonCode(code); - Log.i("TouchscreenController", String.format("%s -> %d", buttonName, code)); + int code = AndroidHostInterface.getInstance().getControllerButtonCode(mControllerType, buttonName); + buttonView.setButtonCode(code); + Log.i("TouchscreenController", String.format("%s -> %d", buttonName, code)); - if (code < 0) { - Log.e("TouchscreenController", String.format("Unknown button name '%s' " + - "for '%s'", buttonName, mControllerType)); - } + if (code < 0) { + Log.e("TouchscreenController", String.format("Unknown button name '%s' " + + "for '%s'", buttonName, mControllerType)); } } @Override public void onButtonStateChanged(TouchscreenControllerButtonView view, boolean pressed) { - if (mHostInterface == null || view.getButtonCode() < 0) + if (view.getButtonCode() < 0) return; - mHostInterface.setControllerButtonState(mControllerIndex, view.getButtonCode(), pressed); + AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex, view.getButtonCode(), pressed); } } diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index e14fc60c2..f6185004d 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -67,4 +67,12 @@ 15 16 + + Digital Controller (Gamepad) + Analog Controller (DualShock) + + + DigitalController + AnalogController + diff --git a/android/app/src/main/res/xml/root_preferences.xml b/android/app/src/main/res/xml/root_preferences.xml index 1b2b84515..a35663a07 100644 --- a/android/app/src/main/res/xml/root_preferences.xml +++ b/android/app/src/main/res/xml/root_preferences.xml @@ -41,11 +41,6 @@ app:defaultValue="@string/settings_console_region_default" app:useSimpleSummaryProvider="true" /> - - + + + + + +