android: implement motion controls via device sensors
This commit is contained in:
parent
ec192c5ca4
commit
1dca51b477
@ -287,12 +287,12 @@ public final class NativeLibrary {
|
||||
/**
|
||||
* Notifies the core emulation that the orientation has changed.
|
||||
*/
|
||||
public static native void NotifyOrientationChange(int layout_option, boolean is_portrait_mode);
|
||||
public static native void NotifyOrientationChange(int layout_option, int rotation);
|
||||
|
||||
/**
|
||||
* Swaps the top and bottom screens.
|
||||
*/
|
||||
public static native void SwapScreens(boolean swap_screens, boolean is_portrait_mode);
|
||||
public static native void SwapScreens(boolean swap_screens, int rotation);
|
||||
|
||||
public static boolean isPortraitMode() {
|
||||
return CitraApplication.getAppContext().getResources().getConfiguration().orientation ==
|
||||
|
@ -261,7 +261,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
|
||||
// Override Citra core INI with the one set by our in game menu
|
||||
NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(),
|
||||
getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
|
||||
getWindowManager().getDefaultDisplay().getRotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -461,7 +461,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
EmulationMenuSettings.setSwapScreens(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
NativeLibrary.SwapScreens(isEnabled, getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
|
||||
NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
break;
|
||||
}
|
||||
|
||||
@ -496,8 +497,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
|
||||
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
||||
item.setChecked(true);
|
||||
NativeLibrary.NotifyOrientationChange(layoutOption,
|
||||
getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
|
||||
NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@ add_library(main SHARED
|
||||
id_cache.h
|
||||
native.cpp
|
||||
native.h
|
||||
ndk_motion.cpp
|
||||
ndk_motion.h
|
||||
)
|
||||
|
||||
target_link_libraries(main PRIVATE common core input_common network)
|
||||
|
@ -15,11 +15,13 @@
|
||||
#include "input_common/main.h"
|
||||
#include "input_common/sdl/sdl.h"
|
||||
#include "jni/button_manager.h"
|
||||
#include "jni/ndk_motion.h"
|
||||
|
||||
namespace InputManager {
|
||||
|
||||
static std::shared_ptr<ButtonFactory> button;
|
||||
static std::shared_ptr<AnalogFactory> analog;
|
||||
static std::shared_ptr<NDKMotionFactory> motion;
|
||||
|
||||
// Button Handler
|
||||
class KeyButton final : public Input::ButtonDevice {
|
||||
@ -305,15 +307,19 @@ std::string GenerateAnalogParamPackage(int axis_id) {
|
||||
void Init() {
|
||||
button = std::make_shared<ButtonFactory>();
|
||||
analog = std::make_shared<AnalogFactory>();
|
||||
motion = std::make_shared<NDKMotionFactory>();
|
||||
Input::RegisterFactory<Input::ButtonDevice>("gamepad", button);
|
||||
Input::RegisterFactory<Input::AnalogDevice>("gamepad", analog);
|
||||
Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion);
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
Input::UnregisterFactory<Input::ButtonDevice>("gamepad");
|
||||
Input::UnregisterFactory<Input::AnalogDevice>("gamepad");
|
||||
Input::UnregisterFactory<Input::MotionDevice>("motion_emu");
|
||||
button.reset();
|
||||
analog.reset();
|
||||
motion.reset();
|
||||
}
|
||||
|
||||
} // namespace InputManager
|
||||
|
@ -36,6 +36,7 @@
|
||||
#include "jni/game_info.h"
|
||||
#include "jni/id_cache.h"
|
||||
#include "jni/native.h"
|
||||
#include "jni/ndk_motion.h"
|
||||
#include "network/network.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/video_core.h"
|
||||
@ -194,18 +195,20 @@ void Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, jobje
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_NotifyOrientationChange(
|
||||
JNIEnv* env, jobject obj, jint layout_option, jboolean is_portrait_mode) {
|
||||
JNIEnv* env, jclass clazz, jint layout_option, jint rotation) {
|
||||
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layout_option);
|
||||
VideoCore::g_renderer->UpdateCurrentFramebufferLayout(is_portrait_mode);
|
||||
VideoCore::g_renderer->UpdateCurrentFramebufferLayout(!(rotation % 2));
|
||||
InputManager::screen_rotation = rotation;
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_SwapScreens(JNIEnv* env, jobject obj,
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_SwapScreens(JNIEnv* env, jclass clazz,
|
||||
jboolean swap_screens,
|
||||
jboolean is_portrait_mode) {
|
||||
jint rotation) {
|
||||
Settings::values.swap_screen = swap_screens;
|
||||
if (VideoCore::g_renderer) {
|
||||
VideoCore::g_renderer->UpdateCurrentFramebufferLayout(is_portrait_mode);
|
||||
VideoCore::g_renderer->UpdateCurrentFramebufferLayout(!(rotation % 2));
|
||||
}
|
||||
InputManager::screen_rotation = rotation;
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_SetUserDirectory(JNIEnv* env, jobject obj,
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
// Function calls from the Java side
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@ -136,10 +138,10 @@ JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_WriteProfileResul
|
||||
jobject obj);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_NotifyOrientationChange(
|
||||
JNIEnv* env, jobject obj, jint layout_option, jboolean is_portrait_mode);
|
||||
JNIEnv* env, jclass clazz, jint layout_option, jint rotation);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SwapScreens(
|
||||
JNIEnv* env, jobject obj, jboolean swap_screens, jboolean is_portrait_mode);
|
||||
JNIEnv* env, jclass clazz, jboolean swap_screens, jint rotation);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2(
|
||||
JNIEnv* env, jclass type, jstring path_);
|
||||
|
160
src/android/app/src/main/jni/ndk_motion.cpp
Normal file
160
src/android/app/src/main/jni/ndk_motion.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include <android/sensor.h>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/vector_math.h"
|
||||
#include "jni/native.h"
|
||||
#include "jni/ndk_motion.h"
|
||||
|
||||
namespace InputManager {
|
||||
|
||||
namespace {
|
||||
using Common::Vec3;
|
||||
}
|
||||
|
||||
class NDKMotion final : public Input::MotionDevice {
|
||||
ASensorManager* sensor_manager = nullptr;
|
||||
ALooper* looper = nullptr;
|
||||
ASensorEventQueue* event_queue;
|
||||
|
||||
mutable std::atomic<Vec3<float>> acceleration{};
|
||||
mutable std::atomic<Vec3<float>> rotation{};
|
||||
static_assert(decltype(acceleration)::is_always_lock_free, "vectors are not lock free");
|
||||
std::thread poll_thread;
|
||||
std::atomic<bool> stop_polling = false;
|
||||
|
||||
static Vec3<float> TransformAxes(Vec3<float> in) {
|
||||
// 3DS Y+ Phone Z+
|
||||
// on | laying |
|
||||
// table | in |
|
||||
// |_______ X- portrait |_______ X+
|
||||
// / mode /
|
||||
// / /
|
||||
// Z- Y-
|
||||
Vec3<float> out;
|
||||
out.y = in.z;
|
||||
// rotations are 90 degrees counter-clockwise from portrait
|
||||
switch (screen_rotation) {
|
||||
case 0:
|
||||
out.x = -in.x;
|
||||
out.z = in.y;
|
||||
break;
|
||||
case 1:
|
||||
out.x = in.y;
|
||||
out.z = in.x;
|
||||
break;
|
||||
case 2:
|
||||
out.x = in.x;
|
||||
out.z = -in.y;
|
||||
break;
|
||||
case 3:
|
||||
out.x = -in.y;
|
||||
out.z = -in.x;
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void Construct(std::chrono::microseconds update_period) {
|
||||
sensor_manager = ASensorManager_getInstanceForPackage("org.citra.citra_emu");
|
||||
looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
|
||||
if (!sensor_manager || !looper) {
|
||||
LOG_CRITICAL(Input, "Could not retrieve sensor manager");
|
||||
return;
|
||||
}
|
||||
event_queue = ASensorManager_createEventQueue(
|
||||
sensor_manager, looper, 0, nullptr, nullptr);
|
||||
if (!event_queue) {
|
||||
LOG_ERROR(Input, "Could not create sensor event queue");
|
||||
return;
|
||||
}
|
||||
const auto init_sensor = [this, update_period](int sensor_type) {
|
||||
ASensorRef sensor = ASensorManager_getDefaultSensor(sensor_manager, sensor_type);
|
||||
if (!sensor) {
|
||||
LOG_ERROR(Input, "Could not find sensor of type {}", sensor_type);
|
||||
return;
|
||||
}
|
||||
int error = ASensorEventQueue_registerSensor(
|
||||
event_queue, sensor,
|
||||
std::max(ASensor_getMinDelay(sensor), static_cast<int>(update_period.count())), 0);
|
||||
if (error < 0)
|
||||
LOG_ERROR(Input, "Registering sensor returned error code {}", error);
|
||||
};
|
||||
|
||||
init_sensor(ASENSOR_TYPE_ACCELEROMETER);
|
||||
init_sensor(ASENSOR_TYPE_GYROSCOPE);
|
||||
}
|
||||
|
||||
void Destruct() {
|
||||
ASensorManager_destroyEventQueue(sensor_manager, event_queue);
|
||||
ALooper_release(looper);
|
||||
}
|
||||
|
||||
void Update() const {
|
||||
ALooper_pollAll(0, nullptr, nullptr, nullptr);
|
||||
ASensorEvent event{};
|
||||
std::optional<Vec3<float>> new_accel{}, new_rot{};
|
||||
while (ASensorEventQueue_getEvents(event_queue, &event, 1) > 0) {
|
||||
if (event.type == ASENSOR_TYPE_ACCELEROMETER) {
|
||||
new_accel.emplace(event.vector.x, event.vector.y, event.vector.z);
|
||||
} else if (event.type == ASENSOR_TYPE_GYROSCOPE){
|
||||
new_rot.emplace(event.vector.x, event.vector.y, event.vector.z);
|
||||
}
|
||||
// occasionally the queue has ASENSOR_TYPE_ADDITIONAL_INFO events
|
||||
// but so far there is no reason to handle them
|
||||
}
|
||||
if (new_accel) {
|
||||
// convert from m/(s^2) to g and invert
|
||||
acceleration = TransformAxes(*new_accel) / -ASENSOR_STANDARD_GRAVITY;
|
||||
}
|
||||
if (new_rot) {
|
||||
// convert from rad/s to deg/s
|
||||
rotation = TransformAxes(*new_rot) * 180.0f / static_cast<float>(M_PI);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
NDKMotion(std::chrono::microseconds update_period, bool asynchronous = false) {
|
||||
if (asynchronous) {
|
||||
poll_thread = std::thread([this, update_period] {
|
||||
Construct(update_period);
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
while (!stop_polling) {
|
||||
Update();
|
||||
std::this_thread::sleep_until(start += update_period);
|
||||
}
|
||||
Destruct();
|
||||
});
|
||||
} else {
|
||||
Construct(update_period);
|
||||
}
|
||||
}
|
||||
|
||||
~NDKMotion() {
|
||||
if (std::thread::id{} == poll_thread.get_id()) {
|
||||
Destruct();
|
||||
} else {
|
||||
stop_polling = true;
|
||||
poll_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<Vec3<float>, Vec3<float>> GetStatus() const override {
|
||||
if (std::thread::id{} == poll_thread.get_id()) {
|
||||
Update();
|
||||
}
|
||||
return {acceleration, rotation};
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Input::MotionDevice> NDKMotionFactory::Create(const Common::ParamPackage& params) {
|
||||
std::chrono::milliseconds update_period{params.Get("update_period", 4)};
|
||||
return std::make_unique<NDKMotion>(update_period);
|
||||
}
|
||||
|
||||
} // namespace InputManager
|
22
src/android/app/src/main/jni/ndk_motion.h
Normal file
22
src/android/app/src/main/jni/ndk_motion.h
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/frontend/input.h"
|
||||
|
||||
namespace InputManager {
|
||||
|
||||
inline std::atomic<int> screen_rotation;
|
||||
|
||||
class NDKMotion;
|
||||
|
||||
class NDKMotionFactory final : public Input::Factory<Input::MotionDevice> {
|
||||
public:
|
||||
/**
|
||||
* Creates a motion device that obtains data from device sensors
|
||||
*/
|
||||
std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override;
|
||||
};
|
||||
} // namespace InputManager
|
@ -76,7 +76,7 @@ void UnregisterFactory(const std::string& name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an input device from given paramters.
|
||||
* Create an input device from given parameters.
|
||||
* @tparam InputDeviceType the type of input devices to create
|
||||
* @param params a serialized ParamPackage string contains all parameters for creating the device
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user