android: implement motion controls via device sensors

This commit is contained in:
Marshall Mohror 2020-03-25 14:53:41 +00:00 committed by bunnei
parent ec192c5ca4
commit 1dca51b477
9 changed files with 210 additions and 14 deletions

View File

@ -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 ==

View File

@ -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);
}

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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_);

View 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

View 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

View File

@ -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
*/