diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 2eee40f6..34166107 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ tools:targetApi="31"> ()); NetworkConfig::LoadOnce(); InputManager::instance().load(); + auto& instance = InputManager::instance(); InitializeGlobalVulkan(); createCemuDirectories(); LatteOverlay_init(); @@ -337,4 +340,19 @@ class EmulationState { onTouchEvent(x, y, isTV, true); } + void onMotion(long timestamp, float gyroX, float gyroY, float gyroZ, float accelX, float accelY, float accelZ) + { + float deltaTime = (timestamp - m_lastMotionTimestamp) * 1e-9f; + m_wiiUMotionHandler.processMotionSample(deltaTime, gyroX, gyroY, gyroZ, accelX * 0.098066f, -accelY * 0.098066f, -accelZ * 0.098066f); + m_lastMotionTimestamp = timestamp; + auto& deviceMotion = InputManager::instance().m_device_motion; + std::scoped_lock lock{deviceMotion.m_mutex}; + deviceMotion.m_motion_sample = m_wiiUMotionHandler.getMotionSample(); + } + void setMotionEnabled(bool enabled) + { + auto& deviceMotion = InputManager::instance().m_device_motion; + std::scoped_lock lock{deviceMotion.m_mutex}; + deviceMotion.m_device_motion_enabled = enabled; + } }; \ No newline at end of file diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp index ba5ebc0d..e6526a65 100644 --- a/src/android/app/src/main/cpp/native-lib.cpp +++ b/src/android/app/src/main/cpp/native-lib.cpp @@ -597,4 +597,16 @@ extern "C" JNIEXPORT jobject JNICALL Java_info_cemu_Cemu_NativeLibrary_getInstalledGamesTitleIds(JNIEnv* env, [[maybe_unused]] jclass clazz) { return JNIUtils::createJavaLongArrayList(env, CafeTitleList::GetAllTitleIds()); +} + +extern "C" JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_onMotion([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong timestamp, jfloat gyroX, jfloat gyroY, jfloat gyroZ, jfloat accelX, jfloat accelY, jfloat accelZ) +{ + s_emulationState.onMotion(timestamp, gyroX, gyroY, gyroZ, accelX, accelY, accelZ); +} + +extern "C" JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setMotionEnabled([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jboolean motionEnabled) +{ + s_emulationState.setMotionEnabled(motionEnabled); } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java index ed298fd2..e9727c9b 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java @@ -395,4 +395,8 @@ public class NativeLibrary { public static native void onTouchMove(int x, int y, boolean isTV); public static native void onTouchUp(int x, int y, boolean isTV); + + public static native void onMotion(long timestamp, float gyroX, float gyroY, float gyroZ, float accelX, float accelY, float accelZ); + + public static native void setMotionEnabled(boolean motionEnabled); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java index 6e468fdf..54d2a705 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java @@ -1,6 +1,7 @@ package info.cemu.Cemu.emulation; import android.annotation.SuppressLint; +import android.content.res.Configuration; import android.graphics.SurfaceTexture; import android.os.Bundle; import android.view.LayoutInflater; @@ -23,12 +24,12 @@ import androidx.fragment.app.Fragment; import info.cemu.Cemu.NativeLibrary; import info.cemu.Cemu.R; import info.cemu.Cemu.databinding.FragmentEmulationBinding; +import info.cemu.Cemu.input.SensorManager; import info.cemu.Cemu.inputoverlay.InputOverlaySettingsProvider; import info.cemu.Cemu.inputoverlay.InputOverlaySurfaceView; @SuppressLint("ClickableViewAccessibility") public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemClickListener { - static private class OnSurfaceTouchListener implements View.OnTouchListener { int currentPointerId = -1; final boolean isTV; @@ -99,8 +100,10 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC private Surface testSurface; private Toast toast; private FragmentEmulationBinding binding; + private boolean isMotionEnabled; private PopupMenu settingsMenu; private InputOverlaySurfaceView inputOverlaySurfaceView; + private SensorManager sensorManager; public EmulationFragment(long gameTitleId) { this.gameTitleId = gameTitleId; @@ -112,14 +115,39 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); var inputOverlaySettingsProvider = new InputOverlaySettingsProvider(requireContext()); + if (sensorManager == null) + sensorManager = new SensorManager(requireContext()); + sensorManager.setIsLandscape(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); overlaySettings = inputOverlaySettingsProvider.getOverlaySettings(); testSurfaceTexture = new SurfaceTexture(0); testSurface = new Surface(testSurfaceTexture); } + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (sensorManager != null) + sensorManager.setIsLandscape(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); + } + + @Override + public void onPause() { + super.onPause(); + sensorManager.pauseListening(); + } + + @Override + public void onResume() { + super.onResume(); + if (isMotionEnabled) + sensorManager.startListening(); + } + @Override public void onDestroy() { super.onDestroy(); + if (sensorManager != null) + sensorManager.pauseListening(); if (testSurface != null) testSurface.release(); if (testSurfaceTexture != null) testSurfaceTexture.release(); } @@ -182,6 +210,14 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC inputOverlaySurfaceView.resetInputs(); return true; } + if (itemId == R.id.enable_motion) { + isMotionEnabled = !item.isChecked(); + if (isMotionEnabled) + sensorManager.startListening(); + else + sensorManager.pauseListening(); + item.setChecked(isMotionEnabled); + } return false; } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/input/InputManager.java b/src/android/app/src/main/java/info/cemu/Cemu/input/InputManager.java index a87d92ea..c537bd1b 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/input/InputManager.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/input/InputManager.java @@ -44,6 +44,8 @@ public class InputManager { return (event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE; } + private static final float MIN_ABS_AXIS_VALUE = 0.33f; + public boolean mapMotionEventToMappingId(int controllerIndex, int mappingId, MotionEvent event) { if (isMotionEventFromJoystick(event)) { InputDevice device = event.getDevice(); @@ -63,7 +65,6 @@ public class InputManager { maxAbsAxisValue = Math.abs(axisValue); } } - float MIN_ABS_AXIS_VALUE = 0.33f; if (maxAbsAxisValue > MIN_ABS_AXIS_VALUE) { NativeLibrary.setControllerMapping(device.getDescriptor(), device.getName(), controllerIndex, mappingId, maxAxis); return true; diff --git a/src/android/app/src/main/java/info/cemu/Cemu/input/SensorManager.java b/src/android/app/src/main/java/info/cemu/Cemu/input/SensorManager.java new file mode 100644 index 00000000..13648e75 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/input/SensorManager.java @@ -0,0 +1,77 @@ +package info.cemu.Cemu.input; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; + +import info.cemu.Cemu.NativeLibrary; + +public class SensorManager implements SensorEventListener { + private final android.hardware.SensorManager sensorManager; + private final Sensor accelerometer; + private final Sensor gyroscope; + private final boolean hasMotionData; + private float gyroX, gyroY, gyroZ; + private float accelX, accelY, accelZ; + private boolean hasAccelData, hasGyroData; + private boolean isLandscape = true; + + public SensorManager(Context context) { + sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + hasMotionData = accelerometer != null && gyroscope != null; + } + + public void startListening() { + if (!hasMotionData) { + return; + } + NativeLibrary.setMotionEnabled(true); + sensorManager.registerListener(this, gyroscope, android.hardware.SensorManager.SENSOR_DELAY_GAME); + sensorManager.registerListener(this, accelerometer, android.hardware.SensorManager.SENSOR_DELAY_GAME); + } + + public void setIsLandscape(boolean isLandscape) { + this.isLandscape = isLandscape; + } + + public void pauseListening() { + if (!hasMotionData) { + return; + } + NativeLibrary.setMotionEnabled(false); + sensorManager.unregisterListener(this); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + accelX = event.values[0]; + accelY = event.values[1]; + accelZ = event.values[2]; + hasAccelData = true; + } + if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { + gyroX = event.values[0]; + gyroY = event.values[1]; + gyroZ = event.values[2]; + hasGyroData = true; + } + if (!hasAccelData || !hasGyroData) { + return; + } + hasAccelData = false; + hasGyroData = false; + if (isLandscape) { + NativeLibrary.onMotion(event.timestamp, gyroY, gyroZ, gyroX, accelY, accelZ, accelX); + return; + } + NativeLibrary.onMotion(event.timestamp, gyroX, gyroY, gyroZ, accelX, accelY, accelZ); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/inputoverlay/InputOverlaySettingsProvider.java b/src/android/app/src/main/java/info/cemu/Cemu/inputoverlay/InputOverlaySettingsProvider.java index 95d5e14f..698a6dd9 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/inputoverlay/InputOverlaySettingsProvider.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/inputoverlay/InputOverlaySettingsProvider.java @@ -37,7 +37,7 @@ public class InputOverlaySettingsProvider { private final SharedPreferences sharedPreferences; - private static final int defaultAlpha = 64; + private static final int DEFAULT_ALPHA = 64; public InputOverlaySettingsProvider(Context context) { this.sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE); @@ -90,7 +90,7 @@ public class InputOverlaySettingsProvider { } private int getAlpha() { - return sharedPreferences.getInt("alpha", defaultAlpha); + return sharedPreferences.getInt("alpha", DEFAULT_ALPHA); } private Optional getRectangle(Input input) { diff --git a/src/android/app/src/main/res/menu/menu_emulation_in_game.xml b/src/android/app/src/main/res/menu/menu_emulation_in_game.xml index 5aacbf2b..3ae28817 100644 --- a/src/android/app/src/main/res/menu/menu_emulation_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_emulation_in_game.xml @@ -1,5 +1,9 @@ + Show pad Replace TV with PAD Installed games + Enable motion \ No newline at end of file diff --git a/src/input/InputManager.cpp b/src/input/InputManager.cpp index d204f0a6..932fe7cc 100644 --- a/src/input/InputManager.cpp +++ b/src/input/InputManager.cpp @@ -967,3 +967,9 @@ void InputManager::update_thread() std::this_thread::yield(); } } + +MotionSample InputManager::get_device_motion_sample() const +{ + std::shared_lock lock(m_device_motion.m_mutex); + return m_device_motion.m_motion_sample; +} \ No newline at end of file diff --git a/src/input/InputManager.h b/src/input/InputManager.h index 97f82ac9..9d016170 100644 --- a/src/input/InputManager.h +++ b/src/input/InputManager.h @@ -98,6 +98,14 @@ public: std::optional get_right_down_mouse_info(bool* is_pad); std::atomic m_mouse_wheel; + struct DeviceMotion + { + mutable std::shared_mutex m_mutex; + MotionSample m_motion_sample; + bool m_device_motion_enabled; + } m_device_motion{}; + + MotionSample get_device_motion_sample() const; private: void update_thread(); diff --git a/src/input/emulated/VPADController.cpp b/src/input/emulated/VPADController.cpp index 90e385f1..21d93c7d 100644 --- a/src/input/emulated/VPADController.cpp +++ b/src/input/emulated/VPADController.cpp @@ -1,5 +1,6 @@ #include "input/emulated/VPADController.h" #include "input/api/Controller.h" +#include #if HAS_SDL #include "input/api/SDL/SDLController.h" #endif // HAS_SDL @@ -237,10 +238,11 @@ void VPADController::update_touch(VPADStatus_t& status) void VPADController::update_motion(VPADStatus_t& status) { - if (has_motion()) + auto& input_manager = InputManager::instance(); + bool has_device_motion = input_manager.m_device_motion.m_device_motion_enabled; + if (has_motion() || has_device_motion) { - auto motionSample = get_motion_data(); - + MotionSample motionSample = has_device_motion ? input_manager.get_device_motion_sample() : get_motion_data(); glm::vec3 acc; motionSample.getVPADAccelerometer(&acc[0]); //const auto& acc = motionSample.getVPADAccelerometer(); @@ -281,7 +283,6 @@ void VPADController::update_motion(VPADStatus_t& status) } bool pad_view; - auto& input_manager = InputManager::instance(); if (const auto right_mouse = input_manager.get_right_down_mouse_info(&pad_view)) { const Vector2 mousePos(right_mouse->x, right_mouse->y);