From fcb511bbca77b0926ef9f7865056129ba44fb97f Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 20 Jul 2023 16:33:43 +0000 Subject: [PATCH] android - add physical controller support add performance hints expand full screen to behind cutouts fix touch, add toggle for virtual gamepad remove safe area margins --- src/LibRyujinx/Android/JniExportedMethods.cs | 11 ++ src/LibRyujinx/LibRyujinx.Graphics.cs | 5 + .../app/src/main/AndroidManifest.xml | 1 + src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h | 6 + .../app/src/main/cpp/ryujinx.cpp | 60 ++++++ .../org/ryujinx/android/GameController.kt | 26 ++- .../main/java/org/ryujinx/android/GameHost.kt | 57 +++--- .../java/org/ryujinx/android/MainActivity.kt | 46 ++++- .../org/ryujinx/android/PerformanceManager.kt | 49 +++++ .../android/PhysicalControllerManager.kt | 69 +++++++ .../android/viewmodels/MainViewModel.kt | 17 +- .../android/viewmodels/QuickSettings.kt | 2 + .../android/viewmodels/SettingsViewModel.kt | 8 +- .../org/ryujinx/android/views/HomeViews.kt | 2 + .../org/ryujinx/android/views/MainView.kt | 175 +++++++++++++++++- .../org/ryujinx/android/views/SettingViews.kt | 33 +++- 16 files changed, 525 insertions(+), 42 deletions(-) create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index 782b05c11..f9bf65434 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -36,6 +36,12 @@ namespace LibRyujinx [DllImport("libryujinxjni")] private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); + [DllImport("libryujinxjni")] + internal extern static void setRenderingThread(); + + [DllImport("libryujinxjni")] + internal extern static void onFrameEnd(double time); + public delegate IntPtr JniCreateSurface(IntPtr native_surface, IntPtr instance); [UnmanagedCallersOnly(EntryPoint = "JNI_OnLoad")] @@ -279,6 +285,11 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")] public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj) { + SetSwapBuffersCallback(() => + { + var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime(); + onFrameEnd(time); + }); RunLoop(); } diff --git a/src/LibRyujinx/LibRyujinx.Graphics.cs b/src/LibRyujinx/LibRyujinx.Graphics.cs index 8ce349806..d3bac5a21 100644 --- a/src/LibRyujinx/LibRyujinx.Graphics.cs +++ b/src/LibRyujinx/LibRyujinx.Graphics.cs @@ -112,6 +112,11 @@ namespace LibRyujinx _isActive = true; + if (Ryujinx.Common.SystemInfo.SystemInfo.IsBionic) + { + setRenderingThread(); + } + while (_isActive) { if (_isStopped) diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index 778370752..d11615d69 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:largeHeap="true" + android:appCategory="game" android:theme="@style/Theme.RyujinxAndroid" tools:targetApi="31"> + +jmethodID _updateFrameTime; +JNIEnv* _rendererEnv = nullptr; + +std::chrono::time_point _currentTimePoint; + +JNIEnv* getEnv(bool isRenderer){ + JNIEnv* env; + if(isRenderer){ + env = _rendererEnv; + } + + if(env != nullptr) + return env; + + auto result = _vm->AttachCurrentThread(&env, NULL); + + return env; +} + +void detachEnv(){ + auto result = _vm->DetachCurrentThread(); +} extern "C" { @@ -128,4 +153,39 @@ jstring createString( } +} +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_MainActivity_getRenderingThreadId(JNIEnv *env, jobject thiz) { + return _currentRenderingThreadId; +} +extern "C" +void setRenderingThread(){ + auto currentId = pthread_self(); + + _currentRenderingThreadId = currentId; + _renderingThreadId = currentId; + + _currentTimePoint = std::chrono::high_resolution_clock::now(); +} +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_MainActivity_initVm(JNIEnv *env, jobject thiz) { + JavaVM* vm = nullptr; + auto success = env->GetJavaVM(&vm); + _vm = vm; + _mainActivity = thiz; + _mainActivityClass = env->GetObjectClass(thiz); +} + +extern "C" +void onFrameEnd(double time){ + auto env = getEnv(true); + auto cl = env->FindClass("org/ryujinx/android/MainActivity"); + _updateFrameTime = env->GetStaticMethodID( cl , "updateRenderSessionPerformance", "(J)V"); + + auto now = std::chrono::high_resolution_clock::now(); + auto nano = std::chrono::duration_cast(now-_currentTimePoint).count(); + env->CallStaticVoidMethod(cl, _updateFrameTime, + nano); } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt index d3c5baa1b..5cc1e20c3 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.flowWithLifecycle @@ -45,9 +46,18 @@ typealias GamePad = RadialGamePad typealias GamePadConfig = RadialGamePadConfig class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) { + private var controllerView: View? = null var leftGamePad: GamePad var rightGamePad: GamePad var controllerId: Int = -1 + val isVisible : Boolean + get() { + controllerView?.apply { + return this.isVisible + } + + return false; + } init { leftGamePad = GamePad(generateConfig(true), 16f, activity) @@ -65,7 +75,6 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = @Composable fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit { - AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)}) @@ -81,14 +90,25 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = } } - private fun Create(context: Context) : View + private fun Create(context: Context) : View { var inflator = LayoutInflater.from(context); var view = inflator.inflate(R.layout.game_layout, null) view.findViewById(R.id.leftcontainer)!!.addView(leftGamePad); view.findViewById(R.id.rightcontainer)!!.addView(rightGamePad); - return view as View + controllerView = view + + return controllerView as View + } + + fun setVisible(isVisible: Boolean){ + controllerView?.apply { + this.isVisible = isVisible + + if(isVisible) + connect() + } } fun connect(){ diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt index a79306c8f..6cc28cc33 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -1,13 +1,10 @@ package org.ryujinx.android import android.content.Context -import android.os.ParcelFileDescriptor +import android.os.Build import android.view.MotionEvent import android.view.SurfaceHolder import android.view.SurfaceView -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.QuickSettings @@ -15,6 +12,7 @@ import kotlin.concurrent.thread import kotlin.math.roundToInt class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { + private var _renderingThreadWatcher: Thread? = null private var _height: Int = 0 private var _width: Int = 0 private var _updateThread: Thread? = null @@ -35,26 +33,6 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo holder.addCallback(this) } - override fun onTouchEvent(event: MotionEvent?): Boolean { - if (_isStarted) - return when (event!!.actionMasked) { - MotionEvent.ACTION_MOVE -> { - _nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt()) - true - } - MotionEvent.ACTION_DOWN -> { - _nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt()) - true - } - MotionEvent.ACTION_UP -> { - _nativeRyujinx.inputReleaseTouchPoint() - true - } - else -> super.onTouchEvent(event) - } - return super.onTouchEvent(event) - } - override fun surfaceCreated(holder: SurfaceHolder) { } @@ -132,7 +110,16 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo _nativeRyujinx.inputInitialize(width, height) - controller.connect() + if(!settings.useVirtualController){ + controller.setVisible(false) + } + else{ + controller.connect() + } + + mainViewModel.activity.physicalControllerManager.connect() + + // _nativeRyujinx.graphicsRendererSetSize( surfaceHolder.surfaceFrame.width(), @@ -159,7 +146,25 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo } private fun runGame() : Unit{ + // RenderingThreadWatcher + _renderingThreadWatcher = thread(start = true) { + var threadId = 0L; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + mainViewModel.performanceManager?.enable() + while (_isStarted) { + Thread.sleep(1000) + var newthreadId = mainViewModel.activity.getRenderingThreadId() + + if (threadId != newthreadId) { + mainViewModel.performanceManager?.closeCurrentRenderingSession() + } + threadId = newthreadId; + if (threadId != 0L) { + mainViewModel.performanceManager?.initializeRenderingSession(threadId) + } + } + } + } _nativeRyujinx.graphicsRendererRunLoop() } - } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index 422b05fed..00ab1b201 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -1,5 +1,6 @@ package org.ryujinx.android +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo @@ -8,6 +9,8 @@ import android.media.AudioManager import android.os.Build import android.os.Bundle import android.os.Environment +import android.view.KeyEvent +import android.view.MotionEvent import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -29,28 +32,43 @@ import org.ryujinx.android.views.MainView class MainActivity : ComponentActivity() { - private var mainViewModel: MainViewModel? = null + var physicalControllerManager: PhysicalControllerManager private var _isInit: Boolean = false var storageHelper: SimpleStorageHelper? = null companion object { + var mainViewModel: MainViewModel? = null var AppPath : String? var StorageHelper: SimpleStorageHelper? = null init { AppPath = "" } + + @JvmStatic + fun updateRenderSessionPerformance(gameTime : Long) + { + if(gameTime <= 0) + return + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + mainViewModel?.performanceManager?.updateRenderingSessionTime(gameTime) + } + } } init { + physicalControllerManager = PhysicalControllerManager(this) storageHelper = SimpleStorageHelper(this) StorageHelper = storageHelper + System.loadLibrary("ryujinxjni") + initVm() } + external fun getRenderingThreadId() : Long + external fun initVm() + fun setFullScreen() :Unit { requestedOrientation = - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; - window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - - WindowCompat.setDecorFitsSystemWindows(window,false) + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE var insets = WindowCompat.getInsetsController(window, window.decorView) @@ -79,6 +97,21 @@ class MainActivity : ComponentActivity() { } } + @SuppressLint("RestrictedApi") + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + event?.apply { + return physicalControllerManager.onKeyEvent(this) + } + return super.dispatchKeyEvent(event) + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { + ev?.apply { + physicalControllerManager.onMotionEvent(this) + } + return super.dispatchGenericMotionEvent(ev) + } + private fun initialize() : Unit { if(_isInit) @@ -94,6 +127,9 @@ class MainActivity : ComponentActivity() { initialize() + window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + WindowCompat.setDecorFitsSystemWindows(window,false) + if(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { !Environment.isExternalStorageManager() } else { diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt new file mode 100644 index 000000000..4568158b5 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt @@ -0,0 +1,49 @@ +package org.ryujinx.android + +import android.os.Build +import android.os.PerformanceHintManager +import androidx.annotation.RequiresApi + +class PerformanceManager(val performanceHintManager: PerformanceHintManager) { + private var _isEnabled: Boolean = false + private var renderingSession: PerformanceHintManager.Session? = null + val DEFAULT_TARGET_NS = 16666666L + + @RequiresApi(Build.VERSION_CODES.S) + fun initializeRenderingSession(threadId : Long){ + if(!_isEnabled || renderingSession != null) + return + + var threads = IntArray(1) + threads[0] = threadId.toInt() + renderingSession = performanceHintManager.createHintSession(threads, DEFAULT_TARGET_NS) + } + + @RequiresApi(Build.VERSION_CODES.S) + fun closeCurrentRenderingSession() { + if (_isEnabled) + renderingSession?.apply { + renderingSession = null + this.close() + } + } + + fun enable(){ + _isEnabled = true + } + + @RequiresApi(Build.VERSION_CODES.S) + fun updateRenderingSessionTime(newTime : Long){ + if(!_isEnabled) + return + + var effectiveTime = newTime + + if(newTime < DEFAULT_TARGET_NS) + effectiveTime = DEFAULT_TARGET_NS + + renderingSession?.apply { + this.reportActualWorkDuration(effectiveTime) + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt new file mode 100644 index 000000000..fe372bdcd --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt @@ -0,0 +1,69 @@ +package org.ryujinx.android + +import android.view.KeyEvent +import android.view.MotionEvent + +class PhysicalControllerManager(val activity: MainActivity) { + private var controllerId: Int = -1 + private var ryujinxNative: RyujinxNative = RyujinxNative() + + fun onKeyEvent(event: KeyEvent) : Boolean{ + if(controllerId != -1) { + var id = GetGamePadButtonInputId(event.keyCode) + + if(id != GamePadButtonInputId.None) { + when (event.action) { + KeyEvent.ACTION_UP -> { + ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId) + } + + KeyEvent.ACTION_DOWN -> { + ryujinxNative.inputSetButtonPressed(id.ordinal, controllerId) + } + } + return true; + } + } + + return false + } + + fun onMotionEvent(ev: MotionEvent) { + if(controllerId != -1) { + if(ev.action == MotionEvent.ACTION_MOVE) { + var leftStickX = ev.getAxisValue(MotionEvent.AXIS_X); + var leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y); + var rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z); + var rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ); + ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId) + ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId) + } + } + } + + fun connect(){ + controllerId = ryujinxNative.inputConnectGamepad(0) + } + + fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId { + return when (keycode) { + KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B + KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A + KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X + KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y + KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder + KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger + KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder + KeyEvent.KEYCODE_BUTTON_R2 -> GamePadButtonInputId.RightTrigger + KeyEvent.KEYCODE_BUTTON_THUMBL -> GamePadButtonInputId.LeftStick + KeyEvent.KEYCODE_BUTTON_THUMBR -> GamePadButtonInputId.RightStick + KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp + KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown + KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft + KeyEvent.KEYCODE_DPAD_RIGHT -> GamePadButtonInputId.DpadRight + KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus + KeyEvent.KEYCODE_BUTTON_SELECT -> GamePadButtonInputId.Minus + else -> GamePadButtonInputId.None + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt index c8d3664b2..bd7ad9442 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -1,18 +1,33 @@ package org.ryujinx.android.viewmodels +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.os.PerformanceHintManager import androidx.compose.runtime.MutableState import androidx.navigation.NavHostController import org.ryujinx.android.GameHost import org.ryujinx.android.MainActivity +import org.ryujinx.android.PerformanceManager +@SuppressLint("WrongConstant") class MainViewModel(val activity: MainActivity) { + var performanceManager: PerformanceManager? = null var selected: GameModel? = null private var gameTimeState: MutableState? = null private var gameFpsState: MutableState? = null private var fifoState: MutableState? = null private var navController : NavHostController? = null - var homeViewModel: HomeViewModel = HomeViewModel(activity, this,) + var homeViewModel: HomeViewModel = HomeViewModel(activity, this) + + init { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + var hintService = + activity.getSystemService(Context.PERFORMANCE_HINT_SERVICE) as PerformanceHintManager + performanceManager = PerformanceManager(hintService) + } + } fun loadGame(game:GameModel) { var controller = navController?: return; diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt index 188e9ec6f..de1d6b320 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt @@ -10,6 +10,7 @@ class QuickSettings(val activity: MainActivity) { var enableDocked: Boolean var enableVsync: Boolean var useNce: Boolean + var useVirtualController: Boolean var isHostMapped: Boolean var enableShaderCache: Boolean var enableTextureRecompression: Boolean @@ -27,5 +28,6 @@ class QuickSettings(val activity: MainActivity) { enableShaderCache = sharedPref.getBoolean("enableShaderCache", true) enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false) resScale = sharedPref.getFloat("resScale", 1f) + useVirtualController = sharedPref.getBoolean("useVirtualController", true) } } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt index 095398921..73ed910ad 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt @@ -26,7 +26,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main ignoreMissingServices: MutableState, enableShaderCache: MutableState, enableTextureRecompression: MutableState, - resScale: MutableState + resScale: MutableState, + useVirtualController: MutableState ) { @@ -39,6 +40,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true) enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false) resScale.value = sharedPref.getFloat("resScale", 1f) + useVirtualController.value = sharedPref.getBoolean("useVirtualController", true) } fun save( @@ -50,7 +52,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main ignoreMissingServices: MutableState, enableShaderCache: MutableState, enableTextureRecompression: MutableState, - resScale: MutableState + resScale: MutableState, + useVirtualController: MutableState ){ var editor = sharedPref.edit() @@ -63,6 +66,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true) editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false) editor.putFloat("resScale", resScale?.value ?: 1f) + editor.putBoolean("useVirtualController", useVirtualController?.value ?: true) editor.apply() } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt index 7962a1c95..811d16a07 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt index d73b90376..ed3487433 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -1,16 +1,30 @@ package org.ryujinx.android.views +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.lifecycleScope @@ -19,8 +33,10 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import org.ryujinx.android.GameController import org.ryujinx.android.GameHost +import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.SettingsViewModel +import kotlin.math.roundToInt class MainView { companion object { @@ -38,16 +54,171 @@ class MainView { @Composable fun GameView(mainViewModel: MainViewModel){ - Box { - var controller = GameController(mainViewModel.activity) + Box(modifier = Modifier.fillMaxSize()) { + val controller = remember { + GameController(mainViewModel.activity) + } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> GameHost(context, controller, mainViewModel) } ) + GameOverlay(mainViewModel, controller) + } + } + + @Composable + fun GameOverlay(mainViewModel: MainViewModel, controller: GameController){ + Box(modifier = Modifier.fillMaxSize()) { GameStats(mainViewModel) + + var ryujinxNative = RyujinxNative() + + // touch surface + Surface(color = Color.Transparent, modifier = Modifier + .fillMaxSize() + .padding(0.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + Thread.sleep(2); + val event = awaitPointerEvent() + + if(controller.isVisible) + continue + + var change = event + .component1() + .firstOrNull() + change?.apply { + var position = this.position + + if (event.type == PointerEventType.Press) { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + } else if (event.type == PointerEventType.Release) { + ryujinxNative.inputReleaseTouchPoint() + + } else if (event.type == PointerEventType.Move) { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + + } + } + } + } + }) { + } controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle) + Row(modifier = Modifier + .align(Alignment.BottomCenter) + .padding(8.dp)) { + IconButton(modifier = Modifier.padding(4.dp),onClick = { + controller.setVisible(!controller.isVisible) + }) { + Icon(imageVector = rememberVideogameAsset(), contentDescription = "Toggle Virtual Pad") + } + } + } + } + @Composable + fun rememberVideogameAsset(): ImageVector { + var primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "videogame_asset", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + fillAlpha = 1f, + stroke = SolidColor(primaryColor), + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(6.25f, 29.792f) + quadToRelative(-1.083f, 0f, -1.854f, -0.792f) + quadToRelative(-0.771f, -0.792f, -0.771f, -1.833f) + verticalLineTo(12.833f) + quadToRelative(0f, -1.083f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) + horizontalLineToRelative(27.5f) + quadToRelative(1.083f, 0f, 1.854f, 0.771f) + quadToRelative(0.771f, 0.771f, 0.771f, 1.854f) + verticalLineToRelative(14.334f) + quadToRelative(0f, 1.041f, -0.771f, 1.833f) + reflectiveQuadToRelative(-1.854f, 0.792f) + close() + moveToRelative(0f, -2.625f) + horizontalLineToRelative(27.5f) + verticalLineTo(12.833f) + horizontalLineTo(6.25f) + verticalLineToRelative(14.334f) + close() + moveToRelative(7.167f, -1.792f) + quadToRelative(0.541f, 0f, 0.916f, -0.375f) + reflectiveQuadToRelative(0.375f, -0.917f) + verticalLineToRelative(-2.791f) + horizontalLineToRelative(2.75f) + quadToRelative(0.584f, 0f, 0.959f, -0.375f) + reflectiveQuadToRelative(0.375f, -0.917f) + quadToRelative(0f, -0.542f, -0.375f, -0.938f) + quadToRelative(-0.375f, -0.395f, -0.959f, -0.395f) + horizontalLineToRelative(-2.75f) + verticalLineToRelative(-2.75f) + quadToRelative(0f, -0.542f, -0.375f, -0.938f) + quadToRelative(-0.375f, -0.396f, -0.916f, -0.396f) + quadToRelative(-0.584f, 0f, -0.959f, 0.396f) + reflectiveQuadToRelative(-0.375f, 0.938f) + verticalLineToRelative(2.75f) + horizontalLineToRelative(-2.75f) + quadToRelative(-0.541f, 0f, -0.937f, 0.395f) + quadTo(8f, 19.458f, 8f, 20f) + quadToRelative(0f, 0.542f, 0.396f, 0.917f) + reflectiveQuadToRelative(0.937f, 0.375f) + horizontalLineToRelative(2.75f) + verticalLineToRelative(2.791f) + quadToRelative(0f, 0.542f, 0.396f, 0.917f) + reflectiveQuadToRelative(0.938f, 0.375f) + close() + moveToRelative(11.125f, -0.5f) + quadToRelative(0.791f, 0f, 1.396f, -0.583f) + quadToRelative(0.604f, -0.584f, 0.604f, -1.375f) + quadToRelative(0f, -0.834f, -0.604f, -1.417f) + quadToRelative(-0.605f, -0.583f, -1.396f, -0.583f) + quadToRelative(-0.834f, 0f, -1.417f, 0.583f) + quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) + quadToRelative(0f, 0.833f, 0.583f, 1.417f) + quadToRelative(0.583f, 0.583f, 1.417f, 0.583f) + close() + moveToRelative(3.916f, -5.833f) + quadToRelative(0.834f, 0f, 1.417f, -0.584f) + quadToRelative(0.583f, -0.583f, 0.583f, -1.416f) + quadToRelative(0f, -0.792f, -0.583f, -1.375f) + quadToRelative(-0.583f, -0.584f, -1.417f, -0.584f) + quadToRelative(-0.791f, 0f, -1.375f, 0.584f) + quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) + quadToRelative(0f, 0.833f, 0.583f, 1.416f) + quadToRelative(0.584f, 0.584f, 1.375f, 0.584f) + close() + moveTo(6.25f, 27.167f) + verticalLineTo(12.833f) + verticalLineToRelative(14.334f) + close() + } + }.build() } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index 3546fe7b5..2a8601477 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.icons.Icons @@ -92,6 +94,9 @@ class SettingViews { var resScale = remember { mutableStateOf(1f) } + var useVirtualController = remember { + mutableStateOf(true) + } if (!loaded.value) { settingsViewModel.initializeState( @@ -100,7 +105,8 @@ class SettingViews { enableVsync, enableDocked, enablePtc, ignoreMissingServices, enableShaderCache, enableTextureRecompression, - resScale + resScale, + useVirtualController ) loaded.value = true } @@ -121,7 +127,8 @@ class SettingViews { ignoreMissingServices, enableShaderCache, enableTextureRecompression, - resScale + resScale, + useVirtualController ) settingsViewModel.navController.popBackStack() }) { @@ -136,7 +143,8 @@ class SettingViews { useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices, enableShaderCache, enableTextureRecompression, - resScale + resScale, + useVirtualController ) } ExpandableView(onCardArrowClick = { }, title = "System") { @@ -431,6 +439,25 @@ class SettingViews { */ } } + ExpandableView(onCardArrowClick = { }, title = "Input") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Show virtual controller", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = useVirtualController.value, onCheckedChange = { + useVirtualController.value = !useVirtualController.value + }) + } + } + } } } }